Skip to content

40 resources register an unusable not_exposed item route, one of them squatting GET /addresses/{addressId} #440

Description

@mattgoud

Summary

An ApiResource that declares an identifier while exposing no item operation makes API Platform register an item route of its own, served by api_platform.action.not_exposed(). Calling it answers:

404 {"detail":"This route does not aim to be called.","class":"ApiPlatform\\Metadata\\Exception\\NotExposedHttpException"}

The URI is then reserved and cannot be implemented by anyone, which is exactly what blocked #427: the core Language resource squatted GET /languages/{langId}, and PrestaShop/PrestaShop#42852 removes it for that reason.

Measured on dev at dcf05ff, with the module installed on a 9.2.x shop, the router serves 40 such routes for this module.

The one that actually costs something

_api_/addresses/{addressId}{._format}_get   GET   /addresses/{addressId}.{_format}
_controller: api_platform.action.not_exposed()

Address declares a single operation, CQRSDelete on /addresses/{addressId}, together with #[ApiProperty(identifier: true)]. So today:

DELETE /addresses/{addressId}   works
GET    /addresses/{addressId}   404 "This route does not aim to be called."
GET    /addresses               works (collection)

A consumer can list addresses and delete one by id, but reading that same id answers a 404 that is not a "not found". And the URI anyone would naturally use to add that read is already taken, so implementing GET /addresses/{addressId} means hitting the same wall #427 hit.

The other 38

The rest generate a snake_case URI derived from the class name, which nobody would request:

/address_lists/{addressId}        /api_client_lists/{apiClientId}
/attribute_group_lists/{...}      /carrier_lists/{carrierId}
/category_lists/{categoryId}      /discount_lists/{discountId}
/product_lists/{productId}        /search_alias_lists/{search}
/tax_rule_lists/{taxRuleId}       /title_lists/{titleId}          ... and 28 more

Low harm individually: they squat nothing anyone wants. They are still worth removing, for two reasons. They answer a confusing 404 to anyone probing the API, and their shape violates the URI convention this repository enforces everywhere else, since CONTEXT.md requires plural kebab-case and these are singular-ish snake_case.

The fix, and the trap in it

Two forms, and they are not interchangeable. I checked both on a running shop.

When the property is not named id, removing the attribute is enough:

// ExampleList.php
- #[ApiProperty(identifier: true)]
  public int $categoryId;

When the property is literally id, removing the attribute changes nothing, because API Platform treats a property called id as the identifier by convention. It takes an explicit:

#[ApiProperty(identifier: false)]
public int $id;

In both cases the collection route survives untouched and answers the identical payload. Verified on FoundShop (#422, the id case) and on CustomerPrivateNote and TransformGuestToCustomer (#428, the $customerId case).

Suggested scope

  1. Address, which is the one with a real consequence. Either expose the item GET, or drop the identifier if reading one address by id is deliberately out of scope.
  2. A sweep over the other 38, mechanical once the rule above is clear.
  3. A line in CONTEXT.md so the next resource does not reintroduce it. The check is one command:
bin/console --app-id=admin-api debug:router --format=json \
  | python3 -c "import json,sys; d=json.load(sys.stdin); [print(v['path']) for v in d.values() if 'not_exposed' in str(v.get('defaults',{}).get('_controller',''))]"

Three open pull requests reintroduce the pattern right now: #422, #428, and this is worth a note on #423 too, which happens to be free of it. Fixing the rule is cheaper than catching it review by review, since neither CI nor the automated pre-review sees it.

Activity

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

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions