api-platform / core

The server component of API Platform: hypermedia and GraphQL APIs in minutes
https://api-platform.com
MIT License
2.45k stars 878 forks source link

[JSON:API] Errors format does not follow specification #3042

Open thomasdom opened 5 years ago

thomasdom commented 5 years ago

PHP version: 7.2 Symfony version: 4.3.1 API-Platform version: 2.4.5

How to reproduce

Example:

Perform a request that will throw an HTTP 400 error due to validation constraint failed

Expected behavior

Error payload respects these following statements (from JSON:API specification on error format and server-side content negotiation):

  • Servers MUST send all JSON:API data in response documents with the header Content-Type: application/vnd.api+json without any media type parameters. [...]
  • Error objects MUST be returned as an array keyed by errors in the top level of a JSON:API document.
  • An error object MAY have the following members:
    • id: a unique identifier for this particular occurrence of the problem.
    • links: a links object containing the following members:
    • about: a link that leads to further details about this particular occurrence of the problem.
    • status: the HTTP status code applicable to this problem, expressed as a string value.
    • code: an application-specific error code, expressed as a string value.
    • title: a short, human-readable summary of the problem that SHOULD NOT change from occurrence to occurrence of the problem, except for purposes of localization.
    • detail: a human-readable explanation specific to this occurrence of the problem. Like title, this field’s value can be localized.
    • source: an object containing references to the source of the error, optionally including any of the following members:
    • pointer: a JSON Pointer [RFC6901] to the associated entity in the request document [e.g. "/data" for a primary data object, or "/data/attributes/title" for a specific attribute].
    • parameter: a string indicating which URI query parameter caused the error.
    • meta: a meta object containing non-standard meta-information about the error.

Example:

Response header Content-Type is application/vnd.api+json

{
    "errors": [
        "links": {
            "about": "https://tools.ietf.org/html/rfc2616#section-10"
        },
        "title": "An error occurred",
        "detail": "countryCode: This value is not a valid country.",
        "source": {
            "pointer": "data/attributes/countryCode"
        }
        "meta": {
            "violations": [
                {
                    "propertyPath": "countryCode",
                    "message": "This value is not a valid country."
                }
            ]
        }
    ]
}

Actual behavior

Error follows the RFC 7807 HTTP Problem specification, which is not compatible with JSON:API error format specs

Example:

Response header Content-Type is application/problem+json; charset=utf-8

{
    "type": "https://tools.ietf.org/html/rfc2616#section-10",
    "title": "An error occurred",
    "detail": "countryCode: This value is not a valid country.",
    "violations": [
        {
            "propertyPath": "countryCode",
            "message": "This value is not a valid country."
        }
    ]
}

I would be glad to submit a PR to fix this, but I'm not experienced enough with API Platform Core development. Help from fellow contributors would be very much appreciated :100:

soyuka commented 5 years ago

Hi! I think that the code responsible for this is https://github.com/api-platform/core/blob/345612c913e1aca6da4f4aa1cd885421ca6385ff/src/JsonApi/Serializer/ErrorNormalizer.php

Feel free to ask for help/open a PR with WIP code !

iamamused commented 5 years ago

Solved this using a decorator:

<?php

namespace App\Decorator;

use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;

final class ApiErrorDecorator implements NormalizerInterface
{
  /** @var DenormalizerInterface|NormalizerInterface  */
  private $decorated;

  public function __construct(NormalizerInterface $decorated)
  {
    if (!$decorated instanceof NormalizerInterface) {
      throw new \InvalidArgumentException(sprintf('The decorated normalizer must implement the %s.', NormalizerInterface::class));
    }

    $this->decorated = $decorated;
  }

  public function supportsNormalization($data, $format = null)
  {
    return $this->decorated->supportsNormalization($data, $format);
  }

  public function normalize($object, $format = null, array $context = [])
  {
    $error = $this->decorated->normalize($object, $format, $context);

    if (!isset($error['links'])) {
      $error['links'] = [];
    }
    $error['links']['about'] = 'https://tools.ietf.org/html/rfc2616#section-10';

    if (!isset($error['meta'])) {
      $error['meta'] = [];
    }

    if (isset($error['trace'])) {
      $error['meta']['trace'] = $error['trace'];
      unset($error['trace']);
    }

    $error['meta']['class'] = get_class($object);

    if ($object instanceof \Symfony\Component\Debug\Exception\FlattenException) {
      $error['status'] = $object->getStatusCode();
      $error['code'] = $object->getCode();
    } else {
      $error['code'] = -1;
    }

    $data = ['errors' => [$error]];

    return $data;
  }
}

then setting the error format in the api_platform.yml

api_platform:
    error_formats:
        jsonapi: ['application/vnd.api+json']

and adding the decorator in services.yml

    App\Decorator\ApiErrorDecorator:
        decorates: 'api_platform.jsonapi.normalizer.error'
        arguments: [ '@App\Decorator\ApiErrorDecorator.inner' ]