jhthorsen / mojolicious-plugin-openapi

OpenAPI / Swagger plugin for Mojolicious
54 stars 42 forks source link

Customize error response schema #198

Closed augensalat closed 3 years ago

augensalat commented 3 years ago

Is it possible to alter the error response schema format that is described in https://metacpan.org/pod/distribution/Mojolicious-Plugin-OpenAPI/lib/Mojolicious/Plugin/OpenAPI/Guides/OpenAPIv3.pod#Default-response-schema and (afaics) is generated in Mojolicious::Plugin::OpenAPI::_before_render?

If not I would like to express my interest in such a feature.

To answer the inevitable question for the use case: The company I currently work for has some sort of API building style guide that defines the format of error responses.

jhthorsen commented 3 years ago

Yes, it right there in the docs that you linked to. In addition, you need to define your own renderer and rewrite the "errors" for the default cases https://metacpan.org/pod/Mojolicious::Plugin::OpenAPI#RENDERER

augensalat commented 3 years ago

Thank you for the quick response but this doesn't look easy to me.

I'm not sure if I understood it correctly, but I started with

$self->plugin(OpenAPI => { ... });
$self->renderer->add_handler(openapi => \&_render);

sub _render {
  # basically copy code from Mojolicious::Plugin::OpenAPI with minor changes for error response
  ...
}

sub _self {
  # c&p from Mojolicious::Plugin::OpenAPI without really understanding it
}

sub _log {
  # c&p from Mojolicious::Plugin::OpenAPI - nice to have
}

That alone is conufsing enough for application code, but after adjusting my openapi spec for my error response format it failed on the first try: 401 Unauthorized, because the error response is hard coded in Mojolicious::Plugin::OpenAPI::Security.

What can I do?

jhthorsen commented 3 years ago

Not sure what you are trying to do, but here is a test that shows what I tried to explain: https://github.com/jhthorsen/mojolicious-plugin-openapi/blob/master/t/basic-custom-renderer.t The important thing is:

plugin OpenAPI => {renderer => \&custom_openapi_renderer, ...};
augensalat commented 3 years ago

Ah, now I understand. So my first very simple custom renderer only replaces "errors" by "error":

sub _render ($c, $data) {
  $data->{error} = delete $data->{errors} if $data->{errors};
  return Mojo::JSON::encode_json($data);
}

But output validation happens before rendering, so my OpenAPI spec has to define an error response as in https://metacpan.org/pod/distribution/Mojolicious-Plugin-OpenAPI/lib/Mojolicious/Plugin/OpenAPI/Guides/OpenAPIv3.pod#Default-response-schema, but the actual response is different from that. In the end I'm violating my own spec.

The test that you mention only works, because there you define the error schema as {"type":"object"} which is basically disabling output validation.

jhthorsen commented 3 years ago

You can redefine the default response schema as mentioned in a previous comment.

augensalat commented 3 years ago

No, does not work. The closest I can achive is the following:

This is my DefaultResonse:

    Error:
      type: object
      properties:
        error:
          $ref: '#/components/schemas/ErrorList'
        status:
          type: integer
          minimum: 400
          maximum: 599
          description: HTTP status code.
      required:
        - error

    ErrorEntity:
      type: object
      properties:
        message:
          type: string
        path:
          oneOf:
            - type: string
            - type: array
      required:
        - message

    ErrorList:
      type: array
      items:
        $ref: '#/components/schemas/ErrorEntity'

My OpenAPI plugin config:

    $self->plugin(OpenAPI => {
        renderer => sub ($c, $data) {
            $data->{error} = delete $data->{errors} if $data->{errors};
            $c->res->headers->content_type('application/json;charset=UTF-8')
                unless $c->res->headers->content_type;

            return Mojo::JSON::encode_json($data);
        },
        url                    => $self->home->rel_file('openapi.yml'),
        schema                 => 'v3',
        default_response_codes => [500],
        default_response_name  => 'Error',
        security => {
            bearerAuth => sub ($c, $definition, $scopes, $cb) {
                my $auth = $c->req->headers->authorization
                    or return $c->$cb('Authorization header is not present');
                my ($token) = $auth =~ /^Bearer\s+(\S+)$/
                    or return $c->$cb('Authorization header is not bearer');

                return $c->$cb($token eq $bearer_token ? undef : 'Wrong bearer token');
            }
        }
    });

And an error is rendered with

    $self->render(
        openapi => {
            status => $e->status,
            errors => [{message => $e->status == 500 ? 'Internal Server Error' : $e->message}]
        },
        status => $e->status,
    );

and my custom renderer above will translate the key errors to error.

This will get me a 500 status for every error response and

[warn] OpenAPI >>> POST /v0/orders [{"message":"\/allOf\/0 Missing property.","path":"\/error"}]

in the log. Why? Because the openapi.yml requires "error", but the validator sees "errors" since the custom renderer comes after the validator.

I found two options:

  1. Neither require "error" nor "errors" in the OpenAPI spec. Then we have the key "error" in the openapi spec, but we actually send "errors" to the validator, which is ignored whereas the (missing) error is not checked.
  2. Change my own error rendering code to have "error" rather than "errors".
    $self->render(
        openapi => {
            status => $e->status,
            error => [{message => $e->status == 500 ? 'Internal Server Error' : $e->message}]
        },
        status => $e->status,
    );

    This works fine for all kind of errors - even those created in Mojolicious::Plugin::OpenAPI::_render (which are munged by my custom renderer), but as I wrote yesterday this fails with Mojolicious::Plugin::OpenAPI::Security, because that module still has hard-wired code to create error responses, which are probably generated before the response validation.

HTTP/1.1 500 Internal Server Error
Content-Length: 84
Server: Mojolicious (Perl)
Content-Type: application/json;charset=UTF-8
Date: Tue, 08 Dec 2020 22:20:16 GMT

{"error":[{"message":"\/allOf\/0 Missing property.","path":"\/error"}],"status":401}

Whichever way I tried - I didn't find a way to customize the error responses in Mojolicious::Plugin::OpenAPI without disabling the output validation - and this is no serious option.

I know that this is tl:dr and hard to follow, but I would be very happy if you could show me where my mistake might be. Otherwise please re-open this issue.

jhthorsen commented 3 years ago

Hi,

Please open an issue, since I'm very bad at replying to emails.

On Wed, Dec 9, 2020 at 7:41 AM Bernhard Graf notifications@github.com wrote:

No, does not work. The closest I can achive is the following:

This is my DefaultResonse:

Error:
  type: object
  properties:
    error:
      $ref: '#/components/schemas/ErrorList'
    status:
      type: integer
      minimum: 400
      maximum: 599
      description: HTTP status code.
  required:
    - error

ErrorEntity:
  type: object
  properties:
    message:
      type: string
    path:
      oneOf:
        - type: string
        - type: array
  required:
    - message

ErrorList:
  type: array
  items:
    $ref: '#/components/schemas/ErrorEntity'

My OpenAPI plugin config:

$self->plugin(OpenAPI => {
    renderer => sub ($c, $data) {
        $data->{error} = delete $data->{errors} if $data->{errors};
        $c->res->headers->content_type('application/json;charset=UTF-8')
            unless $c->res->headers->content_type;

        return Mojo::JSON::encode_json($data);
    },
    url                    => $self->home->rel_file('openapi.yml'),
    schema                 => 'v3',
    default_response_codes => [500],
    default_response_name  => 'Error',
    security => {
        bearerAuth => sub ($c, $definition, $scopes, $cb) {
            my $auth = $c->req->headers->authorization
                or return $c->$cb('Authorization header is not present');
            my ($token) = $auth =~ /^Bearer\s+(\S+)$/
                or return $c->$cb('Authorization header is not bearer');

            return $c->$cb($token eq $bearer_token ? undef : 'Wrong bearer token');
        }
    }
});

And an error is rendered with

$self->render(
    openapi => {
        status => $e->status,
        errors => [{message => $e->status == 500 ? 'Internal Server Error' : $e->message}]
    },
    status => $e->status,
);

and my custom renderer above will translate the key errors to error.

This will get me a 500 status for every error response and

[warn] OpenAPI >>> POST /v0/orders [{"message":"\/allOf\/0 Missing property.","path":"\/error"}]

in the log. Why? Because the openapi.yml requires "error", but the validator sees "errors" since the custom renderer comes after the validator.

I found two options:

  1. Neither require "error" nor "errors" in the OpenAPI spec. Then we have the key "error" in the openapi spec, but we actually send "errors" to the validator, which is ignored whereas the (missing) error is not checked.
  2. Change my own error rendering code to have "error" rather than "errors".

    $self->render( openapi => { status => $e->status, error => [{message => $e->status == 500 ? 'Internal Server Error' : $e->message}] }, status => $e->status, );

This works fine for all kind of errors - even those created in Mojolicious::Plugin::OpenAPI::_render (which are munged by my custom renderer), but as I wrote yesterday this fails with Mojolicious::Plugin::OpenAPI::Security, because that module still has hard-wired code to create error responses, which are probably generated before the response validation.

HTTP/1.1 500 Internal Server Error Content-Length: 84 Server: Mojolicious (Perl) Content-Type: application/json;charset=UTF-8 Date: Tue, 08 Dec 2020 22:20:16 GMT

{"error":[{"message":"\/allOf\/0 Missing property.","path":"\/error"}],"status":401}

Whichever way I tried - I didn't find a way to customize the error responses in Mojolicious::Plugin::OpenAPI without disabling the output validation - and this is no serious option.

I know that this is tl:dr and hard to follow, but I would be very happy if you could show me where my mistake might be. Otherwise please re-open this issue.

— You are receiving this because you modified the open/close state. Reply to this email directly, view it on GitHub https://github.com/jhthorsen/mojolicious-plugin-openapi/issues/198#issuecomment-741156281, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAALFIMQTSWXBSFK5YSFFKDST2TSLANCNFSM4UNTUDWQ .