Skip to content

[master] fix: reject null pathOperator in v4 flow HTTP selectors#17264

Merged
carlos-andres-osorio merged 1 commit into
masterfrom
mergify/bp/master/pr-17260
Jun 2, 2026
Merged

[master] fix: reject null pathOperator in v4 flow HTTP selectors#17264
carlos-andres-osorio merged 1 commit into
masterfrom
mergify/bp/master/pr-17260

Conversation

@mergify
Copy link
Copy Markdown
Contributor

@mergify mergify Bot commented May 30, 2026

This is an automatic copy of pull request #17260 done by Mergify.


Issue

APIM-14252

Why

Creating or updating a v4 HTTP Proxy API with a flow whose HTTP selector has a null pathOperator returned a 500 NPE instead of a 400 validation error. Worse, the write path partially committed before crashing: on POST the API was persisted with an empty flow list, and on PUT an existing API had its name/listeners/endpoints overwritten and its flows wiped before the failure. Any record already stored with a null pathOperator then became unreadable, undeletable, and unupdatable because the read mapper also NPE-d. path: null was already correctly rejected by a @NotNull constraint; pathOperator: null had no equivalent guard.

What changed

  • FlowValidationDomainService — adds a pre-persist check that rejects any HTTP selector with a null pathOperator, throwing InvalidFlowException (400) on the management API create/update path for API-level and plan flows.
  • ApiValidationServiceImpl — routes the v4 HTTP API update path through FlowValidationDomainService (it previously used the deprecated FlowValidationServiceImpl, which had no null-pathOperator guard), so a PUT with a null pathOperator is rejected before persistence instead of partially overwriting the existing API.
  • ValidateApiCRDDomainService — applies the same null-pathOperator rejection on the automation/CRD import path.
  • InvalidFlowException — new missingPathOperator(flowName) factory carrying the offending flow name.
  • FlowMapper — write side now throws InvalidDataException (400) when mapping a null pathOperator to the repository model, as a defensive backstop; read side coerces a stored null to the model default STARTS_WITH so already-corrupt records load, delete, and self-heal on next save instead of returning 500.

Affected endpoints

The fix touches both the write/validation path (now returns 400) and the read path (now self-heals corrupt records). The same code is reached from several endpoints, not just POST /apis:

flowchart LR
    subgraph WRITE["Write / validation — now 400 (was 500 + partial persist)"]
        direction TB
        POSTAPI["POST /management/v2/.../apis"]
        PUTAPI["PUT /management/v2/.../apis/{apiId}"]
        POSTPLAN["POST /management/v2/.../apis/{apiId}/plans"]
        PUTPLAN["PUT /management/v2/.../apis/{apiId}/plans/{planId}"]
        CRD["PUT /automation/.../apis  (CRD import)"]
    end

    subgraph READ["Read / export — now coerces null to STARTS_WITH (was 500)"]
        direction TB
        GETAPI["GET /management/v2/.../apis & /apis/{apiId}"]
        EXPORT["GET /management/v2/.../apis/{apiId}/_export/definition"]
        GETPLANS["GET /management/v2/.../apis/{apiId}/plans"]
        DELAPI["DELETE /management/v2/.../apis/{apiId}"]
        GETCRD["GET /automation/.../apis/{hrid}"]
    end

    FVDS["FlowValidationDomainService\n.checkHttpSelectorsPathOperator()"]
    VCRD["ValidateApiCRDDomainService\n.checkApiFlowsPathOperator()"]
    MAPW["FlowMapper.toRepository()\n(write — throws InvalidDataException)"]
    MAPR["FlowMapper.toDefinition()\n(read — defaults to STARTS_WITH)"]

    POSTAPI --> FVDS
    PUTAPI --> FVDS
    POSTPLAN --> FVDS
    PUTPLAN --> FVDS
    CRD --> VCRD
    FVDS --> MAPW
    VCRD --> MAPW

    GETAPI --> MAPR
    EXPORT --> MAPR
    GETPLANS --> MAPR
    DELAPI --> MAPR
    GETCRD --> MAPR
Loading
Path Endpoints Effect after fix
FlowValidationDomainService POST/PUT /apis, POST/PUT /apis/{apiId}/plans Rejects null pathOperator with 400
ValidateApiCRDDomainService PUT /automation/.../apis (CRD import) Rejects null pathOperator with 400
FlowMapper write side All create/update endpoints above Defensive 400 backstop instead of NPE
FlowMapper read side GET/DELETE /apis, _export/definition, GET /apis/{apiId}/plans, automation GET Coerces stored null to STARTS_WITH; corrupt records load/delete/self-heal

User-facing impact

  • Behaviour change: a v4 HTTP flow selector with a null pathOperator now returns 400 with a validation error instead of 500, on the management API (create/update for both APIs and plans) and the automation/CRD endpoint. No more partial persistence of corrupt API records.
  • Recovery: APIs already persisted with a null pathOperator are now readable, listable, exportable and deletable again (the operator defaults to STARTS_WITH on read) and self-heal on the next save.
  • No API signature, config key, or default changes.

How to test

The fix affects every endpoint in the diagram above — verify a representative write and read endpoint.

Write path (validation): create a v4 HTTP Proxy API with a flow HTTP selector that has pathOperator: null:

curl -s -w "\nHTTP_STATUS:%{http_code}" -X POST "http://localhost:8083/management/v2/environments/DEFAULT/apis" -H "Content-Type: application/json" -u "admin:admin" -d '{"name":"null-operator-test","apiVersion":"1.0","definitionVersion":"V4","type":"PROXY","flows":[{"name":"f1","enabled":true,"selectors":[{"type":"HTTP","path":"/","pathOperator":null,"methods":["GET"]}],"request":[],"response":[]}],"listeners":[{"type":"HTTP","paths":[{"path":"/null-operator-test"}],"entrypoints":[{"type":"http-proxy"}]}],"endpointGroups":[{"name":"default-group","type":"http-proxy","endpoints":[{"name":"default","type":"http-proxy","configuration":{"target":"https://httpbin.org"}}]}]}'

Expect a 400 with flows[].selectors[].pathOperator is required. Repeat with PUT against an existing API and with a plan create/update (POST/PUT /apis/{apiId}/plans) — same result, and the existing API/plan is left untouched. The automation PUT /automation/.../apis import returns 400 as well.

Read path (self-heal): for any API already persisted with a null pathOperator (e.g. created before this fix), GET /apis/{apiId}, _export/definition, and DELETE now succeed instead of returning 500; the selector reads back as STARTS_WITH.

Known issues / follow-up

CRD plan-flow null pathOperator is validated late, not up-front. The up-front CRD guard (ValidateApiCRDDomainService.checkApiFlowsPathOperator) only inspects API-level spec.flows. A null pathOperator in a plan flow is caught downstream by CreatePlanDomainService at persistence time, which has two consequences specific to the CRD import endpoint (PUT /apis/_import/crd):

  • Real import (dryRun=false) returns 400, but the API document is committed before plan creation runs, so a flow-less orphan API (no plans) is left behind — i.e. rejected, but not "nothing persisted." The orphan is clean (no null is stored), reads/deletes normally, and the next reconcile with a corrected spec completes. This is the pre-existing non-transactional CRD partial-commit; this PR removes the 500/data-corruption, not the atomicity gap.
  • Dry-run (dryRun=true) returns 200 (false-positive) for a plan-flow null, because dry-run runs only the up-front API-level check and skips the persistence-time plan validation. An API-level null is still correctly rejected at dry-run (400).

The plain management POST/PUT /apis and POST/PUT /apis/{apiId}/plans paths reject a null pathOperator with 400, leave nothing persisted, and leave any existing API/plan untouched (verified). The gap above is specific to the CRD import/dry-run path and is tracked as follow-up.


Cherry-pick of 611dfe1 has failed:

On branch mergify/bp/master/pr-17260
Your branch is up to date with 'origin/master'.

You are currently cherry-picking commit 611dfe1fe7.
  (fix conflicts and run "git cherry-pick --continue")
  (use "git cherry-pick --skip" to skip this patch)
  (use "git cherry-pick --abort" to cancel the cherry-pick operation)

Changes to be committed:
	modified:   gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/api/domain_service/ValidateApiCRDDomainService.java
	modified:   gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/flow/domain_service/FlowValidationDomainService.java
	modified:   gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/flow/exception/InvalidFlowException.java
	modified:   gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/v4/impl/validation/ApiValidationServiceImpl.java
	modified:   gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/v4/mapper/FlowMapper.java
	modified:   gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/core/api/use_case/ImportApiCRDUseCaseTest.java
	modified:   gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/core/flow/domain_service/FlowValidationDomainServiceTest.java
	new file:   gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/core/flow/exception/InvalidFlowExceptionTest.java
	modified:   gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/rest/api/service/v4/impl/validation/ApiValidationServiceImplTest.java

Unmerged paths:
  (use "git add <file>..." to mark resolution)
	both modified:   gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/core/api/domain_service/ValidateApiCRDDomainServiceTest.java
	both modified:   gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/rest/api/service/v4/mapper/FlowMapperTest.java

To fix up this pull request, you can check it out locally. See documentation: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/checking-out-pull-requests-locally

@mergify mergify Bot requested a review from a team as a code owner May 30, 2026 15:35
@mergify mergify Bot added the conflicts label May 30, 2026
@mergify
Copy link
Copy Markdown
Contributor Author

mergify Bot commented May 30, 2026

Cherry-pick of 611dfe1 has failed:

On branch mergify/bp/master/pr-17260
Your branch is up to date with 'origin/master'.

You are currently cherry-picking commit 611dfe1fe7.
  (fix conflicts and run "git cherry-pick --continue")
  (use "git cherry-pick --skip" to skip this patch)
  (use "git cherry-pick --abort" to cancel the cherry-pick operation)

Changes to be committed:
	modified:   gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/api/domain_service/ValidateApiCRDDomainService.java
	modified:   gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/flow/domain_service/FlowValidationDomainService.java
	modified:   gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/flow/exception/InvalidFlowException.java
	modified:   gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/v4/impl/validation/ApiValidationServiceImpl.java
	modified:   gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/v4/mapper/FlowMapper.java
	modified:   gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/core/api/use_case/ImportApiCRDUseCaseTest.java
	modified:   gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/core/flow/domain_service/FlowValidationDomainServiceTest.java
	new file:   gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/core/flow/exception/InvalidFlowExceptionTest.java
	modified:   gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/rest/api/service/v4/impl/validation/ApiValidationServiceImplTest.java

Unmerged paths:
  (use "git add <file>..." to mark resolution)
	both modified:   gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/core/api/domain_service/ValidateApiCRDDomainServiceTest.java
	both modified:   gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/rest/api/service/v4/mapper/FlowMapperTest.java

To fix up this pull request, you can check it out locally. See documentation: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/checking-out-pull-requests-locally

@carlos-andres-osorio carlos-andres-osorio force-pushed the mergify/bp/master/pr-17260 branch from 6830b9e to 13e9085 Compare June 1, 2026 23:58
… read

The write and validation paths now reject a null pathOperator with a 400
instead of NPE-ing and partially persisting corrupt data, on the management
API and the automation/CRD endpoint, for API-level and plan flows. The read
path coerces a stored null to the model default STARTS_WITH so already-corrupt
records load, delete, and self-heal on next save instead of returning 500.

Why: a v4 HTTP flow selector with a null pathOperator caused a 500 NPE in
FlowMapper instead of a 400 validation error, and left the API
un-readable/un-deletable/un-updatable once persisted.
@carlos-andres-osorio carlos-andres-osorio force-pushed the mergify/bp/master/pr-17260 branch from 13e9085 to b3f5109 Compare June 2, 2026 07:04
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented Jun 2, 2026

@carlos-andres-osorio carlos-andres-osorio merged commit c79774c into master Jun 2, 2026
47 checks passed
@carlos-andres-osorio carlos-andres-osorio deleted the mergify/bp/master/pr-17260 branch June 2, 2026 12:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant