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.

CommentFileSizeAuthor
#14 3093973-14-will-fail.patch1.54 KBrobindh

Issue fork drupal-3093973

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

Grayle created an issue. See original summary.

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.9 was released on November 6 and is the final full bugfix release for the Drupal 8.7.x series. Drupal 8.7.x will not receive any further development aside from security fixes. Sites should prepare to update to 8.8.0 on December 4, 2019. (Drupal 8.8.0-beta1 is available for testing.)

Bug reports should be targeted against the 8.8.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.9.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.7 was released on June 3, 2020 and is the final full bugfix release for the Drupal 8.8.x series. Drupal 8.8.x will not receive any further development aside from security fixes. Sites should prepare to update to Drupal 8.9.0 or Drupal 9.0.0 for ongoing support.

Bug reports should be targeted against the 8.9.x-dev branch from now on, and new development or disruptive changes should be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

JonasSavs’s picture

Hi there,

Normally this could be achieved with:

return (
        new ModifiedResourceResponse(null, $code = 204)
        )->setContent(
                Json::encode($myData)
            );

Best regards.

Version: 8.9.x-dev » 9.2.x-dev

Drupal 8 is end-of-life as of November 17, 2021. There will not be further changes made to Drupal 8. Bugfixes are now made to the 9.3.x and higher branches only. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.2.x-dev » 9.3.x-dev

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.15 was released on June 1st, 2022 and is the final full bugfix release for the Drupal 9.3.x series. Drupal 9.3.x will not receive any further development aside from security fixes. Drupal 9 bug reports should be targeted for the 9.4.x-dev branch from now on, and new development or disruptive changes should be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.9 was released on December 7, 2022 and is the final full bugfix release for the Drupal 9.4.x series. Drupal 9.4.x will not receive any further development aside from security fixes. Drupal 9 bug reports should be targeted for the 9.5.x-dev branch from now on, and new development or disruptive changes should be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

robindh’s picture

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('_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 functionality.

robindh’s picture

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

robindh’s picture

Status: Active » Needs review

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?

smustgrave’s picture

Status: Needs review » Needs work
Issue tags: +Needs Review Queue Initiative, +Needs tests, +Needs issue summary update

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

Also will need a test case showing the issue.

Thanks!

robindh’s picture

Issue summary: View changes
StatusFileSize
new1.54 KB

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.

robindh’s picture

Status: Needs work » Needs review
Issue tags: -Needs issue summary update
smustgrave’s picture

Status: Needs review » Needs work
Issue tags: -Needs tests

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!

Diego_Mow made their first commit to this issue’s fork.

robindh’s picture

Status: Needs work » Needs review

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.

smustgrave’s picture

Status: Needs review » Reviewed & tested by the community

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

catch’s picture

Title: Can not return body during a DELETE request » Allow DELETE requests to return a response body

  • 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...
catch’s picture

Status: Reviewed & tested by the community » Fixed

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

Status: Fixed » Closed (fixed)

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