Skip to content

CD: Return 400 for include paths with unknown relationship names, at any position in the path, in core and the CD plugin #216

Closed

Description

@MoonWorm

The JSON:API spec requires 400 Bad Request for an include path the server can't identify. JsonApi4j silently ignored relationship names that don't exist, at any position in a path: include=pets or include=placeOfBirth.economy returned a 200 with nothing included and no signal to the client. The CD plugin's unsupportedIncludes strategy only covered paths deeper than maxHops.

How it works now: each server checks the relationships of its own resource types, so no shared schema is needed.

  • Core: every read request (by id, multiple, and both relationship reads) is checked against the relationships of the requested type. An unknown first segment gets 400 UNSUPPORTED_INCLUDE, with or without the CD plugin.
  • Later segments: the resolver sends one level of includes per fetch, so the server that owns the type at that point checks the name. The resolver recognises that server's 400 UNSUPPORTED_INCLUDE and maps the name back to the client's full path (economy on countries becomes placeOfBirth.economy).

Error format: UnsupportedIncludeException moves to base (pro.api4.jsonapi4j.exception). It's rendered by the default error handler with one error object per path: code UNSUPPORTED_INCLUDE, source.parameter: include, and meta.path naming the path so callers don't have to parse detail.

{ "errors": [ {
  "status": "400",
  "code": "UNSUPPORTED_INCLUDE",
  "detail": "Resource type 'countries' has no relationship 'economy'",
  "source": { "parameter": "include" },
  "meta": { "path": "placeOfBirth.economy" }
} ] }

jsonapi4j.cd.unsupportedIncludes now covers unknown relationship names as well as depth:

  • FAIL (default): a 400 naming each unsupported path.
  • IGNORE: a 200 with everything that could be resolved, and each unsupported path listed in meta.includedIncomplete as UNSUPPORTED_INCLUDE.
    • First segment: when the app answers 400 UNSUPPORTED_INCLUDE, the CD filter serves the request once more without those paths and reports them through the new CompoundDocsRequest.rejectedIncludes.
    • Later segments: the resolver fetches that batch again without the rejected names, repeating if the backend reports them a few at a time.

Only a 400 made up entirely of UNSUPPORTED_INCLUDE errors counts as rejected includes. A 400 with any other error is passed to the client unchanged, or treated as an ordinary failed fetch when it comes from a downstream service.

Also changed

  • The CD plugin's own 400 mapping is removed, since the default error handler now covers it.
  • ErrorObject gains field-name constants.

Tests

  • Unit tests for the validator, the default error mapping, the parser, the HTTP client, the resolver, the include checker, the filter's retry and the request wrapper.
  • End-to-end tests in CompoundDocsOperationsTests for FAIL, run by the Spring, Quarkus and Servlet samples.
  • A new UnsupportedIncludesIgnoredTests covering IGNORE end to end, run by the Spring sample.

Docs: a new "Unsupported include paths" section on the CD plugin page. The unsupportedIncludes row, the UnsupportedIncludeStrategy Javadoc, the requirements for standalone backends, the exception tree and the Claude Code plugin's configuration reference are updated.

Breaking changes

  • Any JsonApi4j app now answers unknown include names with a 400, even without the CD plugin.
  • UnsupportedIncludeException has moved to pro.api4.jsonapi4j.exception.

Limitations

  • A third-party backend that ignores unknown names, or rejects them in some other form, isn't recognised.
  • Under FAIL, an unknown name deep in a path is only found at that hop, after the earlier hops have already run.

Activity

  1. added a commit that references this issue on Sep 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions