Allow DELETE requests to return a response body

Created on 12 November 2019, about 5 years ago
Updated 4 May 2023, over 1 year ago

Problem/Motivation

I'm working with Core's REST module, and I've extended a custom entity's resource. In the delete method, I want to return some extra data about the deletion, but I always run into the following exception:

Symfony\Component\Serializer\Exception\NotEncodableValueException: Serialization for the format is not supported in Symfony\Component\Serializer\Serializer->serialize() (line 112 of /var/www/html/vendor/symfony/serializer/Serializer.php).
Drupal\rest\EventSubscriber\ResourceResponseSubscriber->Drupal\rest\EventSubscriber\{closure}() (Line: 582)
Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 177)
Drupal\rest\EventSubscriber\ResourceResponseSubscriber->renderResponseBody(Object, Object, Object, NULL) (Line: 76)
Drupal\rest\EventSubscriber\ResourceResponseSubscriber->onResponse(Object, 'kernel.response', Object)
call_user_func(Array, Object, 'kernel.response', Object) (Line: 111)
Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch('kernel.response', Object) (Line: 191)
Symfony\Component\HttpKernel\HttpKernel->filterResponse(Object, Object, 1) (Line: 173)
Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 68)
Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 57)
Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 47)
Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 106)
Drupal\page_cache\StackMiddleware\PageCache->pass(Object, 1, 1) (Line: 85)
Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 49)
Asm89\Stack\Cors->handle(Object, 1, 1) (Line: 47)
Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 52)
Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23)
Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 693)
Drupal\Core\DrupalKernel->handle(Object) (Line: 19)

POST, GET, PATCH all work fine. And all my code inside the resource also works fine. But during the serialization it freaks out. I vaguely remember rest doing some wonky stuff to determine what format to use, and that was a problem for I believe file uploads? Where what was sent doesn't match what should be sent back. I'm thinking this may be the same here, as DELETEs don't receive a body for entities.

I send back a ModifiedResourceResponse with code 202 and some data. I've tried sending back the entity (as I don't delete the entity itself at that point in time), an array of some basic data, a string. The only thing that works is NULL. I've also tried 200, 201, all of 'em basically. No difference.

Steps to reproduce

1. Enable the core rest module
2. Create custom resource plugin with a DELETE method, this function will return a ResourceResponse with content:

/**
 * Provides an example resource.
 *
 * @RestResource(
 *   id = "example_resource",
 *   label = @Translation("Example resource"),
 *   uri_paths = {
 *     "canonical" = "/api/example/{id}",
 *   }
 * )
 */
class ExampleResource extends ResourceBase {

  /**
   * Delete an example entity based on ID.
   *
   * @param string $id
   *   The example ID.
   *
   * @return \Drupal\rest\ResourceResponse
   *   The resource response.
   */
  public function delete(string $id): ResourceResponse {
    return new ResourceResponse(['test123'], 200);
  }

}

3. Configure & enable the plugin:

status: true
dependencies:
  module:
    - example_module
    - serialization
    - user
id: example_resource
plugin_id: example_resource
granularity: resource
configuration:
  methods:
    - DELETE
  formats:
    - json
  authentication:
    - cookie

4. Send delete request to /api/example/123
5. Returns statuscode 500; log: TypeError: Symfony\Component\Serializer\Encoder\ChainEncoder::getEncoder(): Argument #1 ($format) must be of type string, null given, called in /var/www/html/vendor/symfony/serializer/Encoder/ChainEncoder.php on line 49 in Symfony\Component\Serializer\Encoder\ChainEncoder->getEncoder() (line 80 of /var/www/html/vendor/symfony/serializer/Encoder/ChainEncoder.php)

Proposed resolution

Allow custom rest plugin DELETE endpoints to return a response body. According to https://www.rfc-editor.org/rfc/rfc9110.html#section-9.3.5-4, it should be allowed to return a 200 status code with extra information in the response. It's also possible to use statuscode 204, and omit the response body.

Remaining tasks

Test coverage

API changes

Delete routes that are dynamically generated will have the additional _format requirement applied to them.

πŸ› Bug report
Status

Fixed

Version

9.5

Component
RESTΒ  β†’

Last updated about 1 month ago

Created by

πŸ‡§πŸ‡ͺBelgium Grayle

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

  • πŸ‡§πŸ‡ͺBelgium robindh

    So I Just ran into the same issue, when I wanted to return a validation message after failing to delete an entity.

    I'll summarize some debugging in case anyone else stumbles onto this issue:

    ResourceResponseSubscriber::getResponseFormat() will determine the format to return the response in.
    This will check the route requirements that are dynamically generated by the rest module; e.g. _format & _content_type_format.
    For delete routes, format NULL is returned, since dynamically generated DELETE routes do not contain _format or _content_type_format requirements. This is intentional, according to documentation in ResourceResponseSubscriber::renderResponseBody().

    The symfony serializer does not seem to support this (anymore?). Also, the $format parameter is string typed now, so passing a NULL value as format now throws Symfony\Component\Serializer\Encoder\ChainEncoder::getEncoder(): Argument #1 ($format) must be of type string, null given in there. Not just a NotEncodableValueException, like in the original issue report.

    For now, I have fixed the issue on my project by using a RouteSubscriber that alters the dynamically generated route by the rest module. Example snippet:

    class RouteSubscriber extends RouteSubscriberBase {
    
      /**
       * {@inheritdoc}
       */
      protected function alterRoutes(RouteCollection $collection) {
        // Alter the dynamic delete route.
        if ($route = $collection->get('rest.example.DELETE')) {
          $route->setRequirement('_content_type_format', 'json');
          $route->setRequirement('_format', 'json');
        }
      }
    }

    This will make sure a format ('json' in this case) is passed to the symfony serializer; and avoid the fatal error from being thrown.

    The easier solution seems to be expanding the array of request methods in ResourceRoutes::getRoutesForResourceConfig() to include the delete method, not sure yet if this will break some functonality.

  • @robindh opened merge request.
  • πŸ‡§πŸ‡ͺBelgium robindh

    In https://www.drupal.org/project/drupal/issues/3277148 β†’ , documentation was added to indicate _format requirement must always be present when returning a ResourceResponse.

    Created a merge request that will add the _format requirement to DELETE routes, to check which tests will break

  • Status changed to Needs review over 1 year ago
  • πŸ‡§πŸ‡ͺBelgium robindh

    Steps to reproduce on a clean drupal install:

    1. Enable the core rest module
    2. Create custom resource plugin with a DELETE method, this function will return a ResourceResponse with content:
      /**
       * Provides an example resource.
       *
       * @RestResource(
       *   id = "example_resource",
       *   label = @Translation("Example resource"),
       *   uri_paths = {
       *     "canonical" = "/api/example/{id}",
       *   }
       * )
       */
      class ExampleResource extends ResourceBase {
      
        /**
         * Delete an example entity based on ID.
         *
         * @param string $id
         *   The example ID.
         *
         * @return \Drupal\rest\ResourceResponse
         *   The resource response.
         */
        public function delete(string $id): ResourceResponse {
          return new ResourceResponse(['test123'], 200);
        }
      
      }
    3. Configure & enable the plugin:
      status: true
      dependencies:
        module:
          - example_module
          - serialization
          - user
      id: example_resource
      plugin_id: example_resource
      granularity: resource
      configuration:
        methods:
          - DELETE
        formats:
          - json
        authentication:
          - cookie
    4. Send delete request to /api/example/123
    5. Returns statuscode 500; log: TypeError: Symfony\Component\Serializer\Encoder\ChainEncoder::getEncoder(): Argument #1 ($format) must be of type string, null given, called in /var/www/html/vendor/symfony/serializer/Encoder/ChainEncoder.php on line 49 in Symfony\Component\Serializer\Encoder\ChainEncoder->getEncoder() (line 80 of /var/www/html/vendor/symfony/serializer/Encoder/ChainEncoder.php)

    I don't really understand the assumption where DELETE requests cannot contain a response body; according to https://www.rfc-editor.org/rfc/rfc9110.html#section-9.3.5-4, it should be allowed to return a 200 status code with extra information in the response. It's also possible to use statuscode 204, and omit the response body.

    Any maintainers willing to weigh in on this?

  • Status changed to Needs work over 1 year ago
  • πŸ‡ΊπŸ‡ΈUnited States smustgrave

    Issue summary should be updated with proposed solution, any api changes, etc.

    Also will need a test case showing the issue.

    Thanks!

  • πŸ‡§πŸ‡ͺBelgium robindh

    I've reformatted the issue summary and attached a patch containing a test that should fail.
    It mocks a DELETE endpoint rest plugin (no _format route requirement defined), where some content is returned.

  • Status changed to Needs review over 1 year ago
  • Status changed to Needs work over 1 year ago
  • πŸ‡ΊπŸ‡ΈUnited States smustgrave

    Removing the needs tests as #14 shows a valid failure.

    But can the test be added to the MR to show that the change passes the valid test you uploaded

    Thanks!

  • First commit to issue fork.
  • 26:34
    23:39
    Running
  • Open on Drupal.org β†’
    Environment: PHP 8.1 & MySQL 5.7
    15:19
    14:58
    Queued
  • last update over 1 year ago
    30,299 pass, 2 fail
  • Status changed to Needs review over 1 year ago
  • πŸ‡§πŸ‡ͺBelgium robindh

    The test was actually already included in the merge request, since I've removed the $method !== 'DELETE' conditionals from existing tests (testOnResponseWithCacheableResponse & testOnResponseWithUncacheableResponse).

    This will make the existing assertions for the other request methods trigger for DELETE method requests too.

  • last update over 1 year ago
    30,322 pass
  • Status changed to RTBC over 1 year ago
  • πŸ‡ΊπŸ‡ΈUnited States smustgrave

    Reran the tests on the MR just to confirm it was random failure before (it was).

  • last update over 1 year ago
    30,322 pass
  • last update over 1 year ago
    30,322 pass
  • last update over 1 year ago
    30,322 pass
  • Open on Drupal.org β†’
    Environment: PHP 8.1 & MySQL 5.7
    last update over 1 year ago
    Waiting for branch to pass
  • πŸ‡¬πŸ‡§United Kingdom catch
    • catch β†’ committed 04f1dcfa on 10.0.x
      Issue #3093973 by robindh, Diego_Mow, smustgrave, Grayle: Allow DELETE...
    • catch β†’ committed a5b62885 on 10.1.x
      Issue #3093973 by robindh, Diego_Mow, smustgrave, Grayle: Allow DELETE...
    • catch β†’ committed 4de70df5 on 9.5.x
      Issue #3093973 by robindh, Diego_Mow, smustgrave, Grayle: Allow DELETE...
  • Status changed to Fixed over 1 year ago
  • πŸ‡¬πŸ‡§United Kingdom catch

    Committed/pushed to 10.1.x and cherry-picked to 10.0.x and 9.5.x, thanks!

  • Automatically closed - issue fixed for 2 weeks with no activity.

Production build 0.71.5 2024