Subresource Creation Fails When Attempting to Create Only the Subresource
API Platform Version(s) Affected: 3.3.12
Description: Subresource creation using URI templates is not functioning as expected. When attempting to create only the subresource via the parent resource’s identifier in the URI, several issues occur, such as errors when the parent resource doesn’t exist, unexpected overriding of existing subresources, and failures when multiple subresources are involved. This behavior persists across different scenarios and requires attention to properly handle subresource creation through URI templates.
Steps to Reproduce:
- Configure two resources that are related.
- Set up a subresource
POSToperation using the following URI template:resources/{resourceId}/subresource. - Attempt to create only the subresource using the ID of an existing parent resource:
resources/{existingId}/subresource.
Our resource configuration example with some outcomes:
<resource class="Sylius\Component\Core\Model\PromotionCoupon">
<operations>
<operation class="ApiPlatform\Metadata\Post"
uriTemplate="/admin/promotions/{promotionCode}/coupons">
<uriVariables>
<uriVariable parameterName="promotionCode" fromClass="Sylius\Component\Core\Model\Promotion"
fromProperty="coupons"/>
</uriVariables>
</operation>
</operations>
</resource>
We tested several scenarios (for clarity, required payload data is omitted in the examples below):
-
URI:
/promotions/non-existent-code/coupons
Operation:POST
Initial state: None
Expected: Parent resource not found
Actual Result:{ "@context": "/api/v2/contexts/Error", "@type": "hydra:Error", "hydra:title": "An error occurred", "hydra:description": "PropertyAccessor requires a graph of objects or arrays to operate on, but it found type 'NULL' while trying to traverse path 'promotion.code' at property 'code'." } -
URI:
/promotions/autumn/coupons
Operation:POST
Initial state: Promotion exists, no coupons
Expected: Subresource created
Actual Result:{ "@context": "/api/v2/contexts/Error", "@type": "hydra:Error", "hydra:title": "An error occurred", "hydra:description": "PropertyAccessor requires a graph of objects or arrays to operate on, but it found type 'NULL' while trying to traverse path 'promotion.code' at property 'code'." } -
URI:
/promotions/autumn/coupons
Operation:POST
Initial state: Promotion exists, one coupon exists
Expected: Second coupon created
Actual Result:201, but the existing subresource was overridden instead of creating a new one. -
URI:
/promotions/autumn/coupons
Operation:POST
Initial state: Promotion exists, two coupons exist
Expected: Third coupon created
Actual Result:{ "@context": "/api/v2/contexts/Error", "@type": "hydra:Error", "hydra:title": "An error occurred", "hydra:description": "More than one result was found for query although one row or none was expected." }
Workarounds: We observed interesting behavior when allowing the payload to include relation information, although these scenarios are not valid. They might provide insight into the core issue.
-
URI:
/promotions/autumn/coupons
Operation:POST
Initial state: Promotion exists, no coupons
Payload:{ ... "promotion": "api/v2/admin/promotions/autumn" }Expected: Coupon created
Actual Result:- First request: coupon created.
- Second identical request: coupon overridden instead of creating a new one.
-
URI:
/promotions/autumn/coupons
Operation:POST
Initial state: Promotion exists, two coupons
Payload:{ ... "promotion": "api/v2/admin/promotions/autumn" }Expected: Third coupon created
Actual Result:{ "@context": "/api/v2/contexts/Error", "@type": "hydra:Error", "hydra:title": "An error occurred", "hydra:description": "More than one result was found for query although one row or none was expected." } -
URI:
/promotions/different-promotion-code-or-made-up-one/coupons
Operation:POST
Initial state: Promotion exists (autumn promotion), any number of coupons
Payload:{ ... "promotion": "api/v2/admin/promotions/autumn" }Expected: Each request creates a new coupon resource
Actual Result: Expected behavior.
Conclusion:
Scenario seven works, but it’s tricky since the operation is based on the payload rather than URL parameters. The issue seems related to how API Platform resolves links, particularly when the base resource is a subresource (in this example, <resource class="Sylius\Component\Core\Model\PromotionCoupon">).