-5

I want to create an asynchronuous OpenAPI interface. Async jobs return a 202 and a location header to query later.

This is my OpenAPI document:

---
components:
  headers:
    JobLocation:
      description: Job status URL
      schema:
        type: string
    JobRetryAfter:
      description: Query delay in seconds
      schema:
        oneOf:
          - minimum: 1
            type: integer
          - type: string
  schemas:
    ErrorMessage:
      description: An error descriptopm
      example:
        customer_error: You did not provide a password
        message: 'Request error: Password is missing'
      properties:
        customer_error:
          description: Error for consumer
          type: string
        message:
          description: Technical error description
          type: string
      required:
        - message
      type: object
    Job:
      properties:
        code:
          type: integer
        completedAt:
          format: date-time
          nullable: true
          type: string
        createdAt:
          format: date-time
          type: string
        jobId:
          type: string
        result:
          $ref: '#/components/schemas/Result'
        status:
          enum:
            - initiated
            - pending
            - completed
            - failed
          type: string
      required:
        - jobId
        - code
        - status
        - createdAt
      type: object
    JobAccepted:
      example:
        jobId: job-12345
        statusUrl: /jobs/job-12345
      properties:
        jobId:
          type: string
        statusUrl:
          type: string
      type: object
    Notification:
      properties:
        message:
          description: Beschreibung der Benachrichtigung
          type: string
        notificationID:
          description: ein eindeutiger Bezeichner dieser Benachrichtigung
          type: string
        path:
          description: Aktuell unbenutzt
          type: string
        severity:
          description: Einschätzung der Benachrichtigung
          enum:
            - CRITICAL
            - ERROR
            - WARNING
            - INFO
          type: string
      required:
        - notificationID
        - severity
        - message
    PayloadBaseObject:
      properties:
        verb:
          type: string
      required:
        - verb
      type: object
    Result:
      properties:
        notifications:
          items:
            $ref: '#/components/schemas/Notification'
          type: array
        payload:
          items:
            discriminator:
              propertyName: entityType
            oneOf:
              - $ref: '#/components/schemas/SyncMock'
          type: array
      type: object
    SyncMock:
      allOf:
        - $ref: '#/components/schemas/PayloadBaseObject'
        - properties:
            entityType:
              enum:
                - mock-details
              type: string
            reqkey:
              type: string
            result:
              properties:
                async:
                  description: demo
                  type: integer
              type: object
          required:
            - entityType
            - reqkey
          type: object
      description: Demo of a synchronuous API call
  securitySchemes:
    http:
      description: Basic Auth credentials
      scheme: basic
      type: http
info:
  title: mock service
  version: 0.0.1
openapi: 3.0.4
paths:
  /sync:
    get:
      description: Makes a synchronuous API call and waits for the result. There is no timeout.
      operationId: getMockSync
      responses:
        200:
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Job'
          description: Synchronuous job response
      security:
        - http: []
      summary: Mocks a synchronuous REST call
      tags:
        - mock
      x-mojo-to: Mock#sync
  /async:
    get:
      description: Makes an asynchronuous API call and does NOT wait for the result.
      operationId: getMockAsync
      responses:
        202:
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobAccepted'
          description: Asynchronuous job response
      security:
        - http: []
      summary: Mocks an asynchronuous REST call
      tags:
        - mock
      x-mojo-to: Mock#async
  '/job/{jobid}':
    get:
      summary: Get job state
      description: Get job state
      operationId: retrieveJobStatus
      parameters:
        - description: A job ID
          in: path
          name: jobid
          required: true
          schema:
            type: string
      responses:
        200:
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Job'
          description: 'Job done'
        202:
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobAccepted'
          description: 'Job not done yet'
          headers:
            Location:
              $ref: '#/components/headers/JobLocation'
            Retry-After:
              $ref: '#/components/headers/JobRetryAfter'
        404:
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorMessage'
          description: 'Job not found (anymore)'
      x-mojo-to: Job#status
security:
  - http: []
servers:
  - description: Mock APIv2 service
    url: /mock
tags:
  - description: Just mocking
    name: mock

Here is a complete test script to reproduce my issue:

use strict;
use warnings;

use File::Spec;
use FindBin qw< $Bin >;

use Test::More;
use Test::Exception;
use Test::Mojo;

my $api = File::Spec->catfile($Bin => q(test.yaml));
my $t   = Test::Mojo->new(q(Test::APIv2));

my $prefix = q(/mock);
$t->get_ok(qq($prefix/sync))->status_is(200);
$t->get_ok(qq($prefix/async))->status_is(202)->header_exists(q(Location))
    ->header_like(Location => qr/\/job\/[0-9a-f]+$/)
    ->header_exists(q(Retry-after));
my $job = $t->tx->res->json(q(/jobId));
##note(qq(Job ID: $job));

$t->get_ok(qq($prefix/job/$job))->status_is(202);
$t->get_ok(qq($prefix/job/$job))->status_is(200);

done_testing();

package My::App;

use 5.020;    # -signatures flag for Mojolicious
use Mojo::Base 'Mojolicious' => -signatures;
use Carp;
use HTTP::Status qw<>;
use Try::Tiny;

sub startup ($self) {
    ##  avoid (currently) useless warning
    $self->secrets([qw< abc cde >]);

    $self->plugin(q(Config));

    ##  initialize UI
    $self->helper(ui => sub { state $ui = $self->init_ui() });

    ##  initialize OpenAPI plugin
    $self->plugin(
        OpenAPI => {
            ##  TODO make this a factory call to be better testable
            url => $self->config->{api_description},
            ##  make sure the response obeys the scheme
            validate_response => 1,
            log_level         => q(trace),
        }
    );

    $self->helper(
        myrender => sub ($c, $data, %opts) {
            my $status = $data->{code}
                or croak(q("code" is missing in response));

            if (grep({ $status == $_ } (HTTP::Status::HTTP_ACCEPTED))
                and my $jobid = $data->{jobId})
            {
                my $r
                    = $c->url_for(q(retrieveJobStatus) => { jobid => $jobid })
                    ->to_abs;
                $c->res->headers->header(Location => $r);

                my $ra = 3;    ## TODO static here?
                $c->res->headers->header(q(Retry-after) => $ra);
            } ## end if (grep({ $status == ...}))
            $c->render(openapi => $data, status => $status);
        }
    );
} ## end sub startup

sub init_ui ($self) { croak(q(Not interesting here)) }

package My::App::Controller::Job;

use 5.020;    # -signatures flag for Mojolicious
use Mojo::Base "Mojolicious::Controller" => -signatures;

{
    my $c;
    BEGIN { $c = 0 }
    sub get_c { $c++ }
}

sub status ($self) {
    $self = $self->openapi->valid_input
        or return;

    my $jobid  = $self->param(q(jobid));
    my $status = get_c() ? 200 : 202;
    my $r
        = $status == 200
        ? {
        'jobId'       => $jobid,
        'code'        => $status,
        'createdAt'   => '2025-11-14T16:24:44Z',
        'completedAt' => '2025-11-14T16:24:46Z',
        'status'      => 'completed',
        'result'      => {
            'payload' => [
                {
                    'reqkey'     => 'ent1',
                    'result'     => { 'async' => 0 },
                    'entityType' => 'mock-details',
                    'verb'       => 'sync'
                }
            ]
        },
        }
        : {
        'jobId'     => $jobid,
        'code'      => $status,
        'createdAt' => '2025-11-14T16:24:44Z',
        'status'    => 'initiated',
        };

    $self->myrender($r, status => $status,);
} ## end sub status

package Test::APIv2;

use 5.020;    # -signatures flag for Mojolicious
use Mojo::Base 'My::App' => -signatures;

sub startup ($self) {
    $self->SUPER::startup;
    $self->routes->namespaces(
        [
            qw<
                My::App::Controller
                Test::APIv2::Controller
            >
        ]
    );
} ## end sub startup

sub init_ui ($self) {return}

package Test::APIv2::Controller::Mock;

use 5.020;    # -signatures flag for Mojolicious
use Mojo::Base "Mojolicious::Controller" => -signatures;

use Carp;
use Try::Tiny;

sub sync ($self) {
    $self = $self->openapi->valid_input
        or return;

    $self->myrender(
        {
            'createdAt'   => '2025-11-14T16:24:44Z',
            'code'        => 200,
            'completedAt' => '2025-11-14T16:24:46Z',
            'jobId'       =>
                '7092005578957c4aa8695cf304a8f15eea34c92bd22ec62cc6b5721efaa74676',
            'result' => {
                'payload' => [
                    {
                        'reqkey'     => 'ent1',
                        'result'     => { 'async' => 0 },
                        'entityType' => 'mock-details',
                        'verb'       => 'sync'
                    }
                ]
            },
            'status' => 'completed'
        }
    );
} ## end sub sync

sub async ($self) {
    $self = $self->openapi->valid_input
        or return;

    $self->myrender(
        {
            "code"      => 202,
            "createdAt" => "2025-11-14T16:24:46Z",
            "jobId"     =>
                "58b074fbf5e421e0d79df4a5d38ef2e970a49ff51d06bd52718bacec483007f2",
            "status" => "initiated"
        }
    );
} ## end sub async

Put this into a directory (e.g. /tmp/mojo) and add the YAML (test.yaml) from above and a configfile test-a_p_iv2.conf to this directory:

{ api_description => q(test.yaml), }
##  vim:set filetype=perl:

Then execute

prove -v mock.t

The first tests (sync and async) work as expected, but when I call the job endpoint, there is a stacktrace:

[2025-11-14 18:01:49.92515] [1276658] [error] [DyHG8MMADFrN] You have to call resolve() before validate() to lookup "#/components/headers/JobLocation". at /usr/share/perl5/JSON/Validator/Schema/Draft201909.pm line 61.
    JSON::Validator::Schema::Draft201909::_state(JSON::Validator::Schema::OpenAPIv3=HASH(0x5ffda54c88a8), HASH(0x5ffda58eb580), "schema", HASH(0x5ffda5fb4b28)) called at /usr/share/perl5/JSON/Validator/Schema.pm line 155
    JSON::Validator::Schema::validate(JSON::Validator::Schema::OpenAPIv3=HASH(0x5ffda54c88a8), Mojo::URL=HASH(0x5ffda5fb2d98), HASH(0x5ffda5fb4b28)) called at /usr/share/perl5/JSON/Validator/Schema/OpenAPIv2.pm line 374
    JSON::Validator::Schema::OpenAPIv2::_validate_request_or_response(JSON::Validator::Schema::OpenAPIv3=HASH(0x5ffda54c88a8), "response", ARRAY(0x5ffda5fb24f8), HASH(0x5ffda584c138)) called at /usr/share/perl5/JSON/Validator/Schema/OpenAPIv2.pm line 174
    JSON::Validator::Schema::OpenAPIv2::validate_response(JSON::Validator::Schema::OpenAPIv3=HASH(0x5ffda54c88a8), ARRAY(0x5ffda5fb2f60), HASH(0x5ffda5fb51e8)) called at /usr/share/perl5/Mojolicious/Plugin/OpenAPI.pm line 256
    Mojolicious::Plugin::OpenAPI::_render(Mojolicious::Renderer=HASH(0x5ffda50396e8), My::App::Controller::Job=HASH(0x5ffda5fb2318), SCALAR(0x5ffda521f090), HASH(0x5ffda5fc4c40)) called at /usr/share/perl5/Mojolicious/Renderer.pm line 229
    Mojolicious::Renderer::_render_template(Mojolicious::Renderer=HASH(0x5ffda50396e8), My::App::Controller::Job=HASH(0x5ffda5fb2318), SCALAR(0x5ffda521f090), HASH(0x5ffda5fc4c40)) called at /usr/share/perl5/Mojolicious/Renderer.pm line 108
    Mojolicious::Renderer::render(Mojolicious::Renderer=HASH(0x5ffda50396e8), My::App::Controller::Job=HASH(0x5ffda5fb2318)) called at /usr/share/perl5/Mojolicious/Controller.pm line 149
    Mojolicious::Controller::render(My::App::Controller::Job=HASH(0x5ffda5fb2318), "openapi", HASH(0x5ffda5fb2f30), "status", 202) called at mock.t line 73
    My::App::__ANON__(My::App::Controller::Job=HASH(0x5ffda5fb2318), HASH(0x5ffda5fb2f30), "status", 202) called at /usr/share/perl5/Mojolicious/Controller.pm line 25
    Mojolicious::Controller::_Dynamic::myrender(My::App::Controller::Job=HASH(0x5ffda5fb2318), HASH(0x5ffda5fb2f30), "status", 202) called at mock.t line 123
    My::App::Controller::Job::status(My::App::Controller::Job=HASH(0x5ffda5fb2318)) called at /usr/share/perl5/Mojolicious.pm line 193
    Mojolicious::_action(undef, My::App::Controller::Job=HASH(0x5ffda5fb2318), CODE(0x5ffda53ca6f0), 1) called at /usr/share/perl5/Mojolicious/Plugins.pm line 15
    Mojolicious::Plugins::__ANON__() called at /usr/share/perl5/Mojolicious/Plugins.pm line 18
    Mojolicious::Plugins::emit_chain(Mojolicious::Plugins=HASH(0x5ffda537a810), "around_action", My::App::Controller::Job=HASH(0x5ffda5fb2318), CODE(0x5ffda53ca6f0), 1) called at /usr/share/perl5/Mojolicious/Routes.pm line 88
    Mojolicious::Routes::_action(Test::APIv2=HASH(0x5ffda5039430), My::App::Controller::Job=HASH(0x5ffda5fb2318), CODE(0x5ffda53ca6f0), 1) called at /usr/share/perl5/Mojolicious/Routes.pm line 161
    Mojolicious::Routes::_controller(Mojolicious::Routes=HASH(0x5ffda3666430), Mojolicious::Controller=HASH(0x5ffda5fb1750), HASH(0x5ffda5fbb368), 1) called at /usr/share/perl5/Mojolicious/Routes.pm line 44
    Mojolicious::Routes::continue(Mojolicious::Routes=HASH(0x5ffda3666430), Mojolicious::Controller=HASH(0x5ffda5fb1750)) called at /usr/share/perl5/Mojolicious/Routes.pm line 52
    Mojolicious::Routes::dispatch(Mojolicious::Routes=HASH(0x5ffda3666430), Mojolicious::Controller=HASH(0x5ffda5fb1750)) called at /usr/share/perl5/Mojolicious.pm line 127
    Mojolicious::dispatch(Test::APIv2=HASH(0x5ffda5039430), Mojolicious::Controller=HASH(0x5ffda5fb1750)) called at /usr/share/perl5/Mojolicious.pm line 136
    Mojolicious::__ANON__(undef, Mojolicious::Controller=HASH(0x5ffda5fb1750)) called at /usr/share/perl5/Mojolicious/Plugins.pm line 15
    Mojolicious::Plugins::__ANON__() called at /usr/share/perl5/Mojolicious.pm line 203
    eval {...} called at /usr/share/perl5/Mojolicious.pm line 203
    Mojolicious::_exception(CODE(0x5ffda5fb2b58), Mojolicious::Controller=HASH(0x5ffda5fb1750)) called at /usr/share/perl5/Mojolicious/Plugins.pm line 15
    Mojolicious::Plugins::__ANON__() called at /usr/share/perl5/Mojolicious/Plugins.pm line 18
    Mojolicious::Plugins::emit_chain(Mojolicious::Plugins=HASH(0x5ffda537a810), "around_dispatch", Mojolicious::Controller=HASH(0x5ffda5fb1750)) called at /usr/share/perl5/Mojolicious.pm line 141
    Mojolicious::handler(Test::APIv2=HASH(0x5ffda5039430), Mojo::Transaction::HTTP=HASH(0x5ffda5f3a4c8)) called at /usr/share/perl5/Mojo/Server.pm line 72
    Mojo::Server::__ANON__(Mojo::Server::Daemon=HASH(0x5ffda5f24a00), Mojo::Transaction::HTTP=HASH(0x5ffda5f3a4c8)) called at /usr/share/perl5/Mojo/EventEmitter.pm line 15
    Mojo::EventEmitter::emit(Mojo::Server::Daemon=HASH(0x5ffda5f24a00), "request", Mojo::Transaction::HTTP=HASH(0x5ffda5f3a4c8)) called at /usr/share/perl5/Mojo/Server/Daemon.pm line 103
    Mojo::Server::Daemon::__ANON__(Mojo::Transaction::HTTP=HASH(0x5ffda5f3a4c8)) called at /usr/share/perl5/Mojo/EventEmitter.pm line 15
    Mojo::EventEmitter::emit(Mojo::Transaction::HTTP=HASH(0x5ffda5f3a4c8), "request") called at /usr/share/perl5/Mojo/Transaction/HTTP.pm line 60
    Mojo::Transaction::HTTP::server_read(Mojo::Transaction::HTTP=HASH(0x5ffda5f3a4c8), "GET /mock/job/58b074fbf5e421e0d79df4a5d38ef2e970a49ff51d06bd5"...) called at /usr/share/perl5/Mojo/Server/Daemon.pm line 224
    Mojo::Server::Daemon::_read(Mojo::Server::Daemon=HASH(0x5ffda5f24a00), "508eabecdacc338f5072fe4f078ab562", "GET /mock/job/58b074fbf5e421e0d79df4a5d38ef2e970a49ff51d06bd5"...) called at /usr/share/perl5/Mojo/Server/Daemon.pm line 202
    Mojo::Server::Daemon::__ANON__(Mojo::IOLoop::Stream=HASH(0x5ffda5f41d70)) called at /usr/share/perl5/Mojo/EventEmitter.pm line 15
    Mojo::EventEmitter::emit(Mojo::IOLoop::Stream=HASH(0x5ffda5f41d70), "read", "GET /mock/job/58b074fbf5e421e0d79df4a5d38ef2e970a49ff51d06bd5"...) called at /usr/share/perl5/Mojo/IOLoop/Stream.pm line 109
    Mojo::IOLoop::Stream::_read(Mojo::IOLoop::Stream=HASH(0x5ffda5f41d70)) called at /usr/share/perl5/Mojo/IOLoop/Stream.pm line 57
    Mojo::IOLoop::Stream::__ANON__(Mojo::Reactor::EV=HASH(0x5ffda4b14e50)) called at /usr/share/perl5/Mojo/Reactor/Poll.pm line 141
    eval {...} called at /usr/share/perl5/Mojo/Reactor/Poll.pm line 141
    Mojo::Reactor::Poll::_try(Mojo::Reactor::EV=HASH(0x5ffda4b14e50), "I/O watcher", CODE(0x5ffda5f422c8), 0) called at /usr/share/perl5/Mojo/Reactor/EV.pm line 54
    Mojo::Reactor::EV::__ANON__(EV::IO=SCALAR(0x5ffda5f42118), 1) called at /usr/share/perl5/Mojo/Reactor/EV.pm line 32
    eval {...} called at /usr/share/perl5/Mojo/Reactor/EV.pm line 32
    Mojo::Reactor::EV::start(Mojo::Reactor::EV=HASH(0x5ffda4b14e50)) called at /usr/share/perl5/Mojo/IOLoop.pm line 134
    Mojo::IOLoop::start(Mojo::IOLoop=HASH(0x5ffda4ae5fd0)) called at /usr/share/perl5/Mojo/UserAgent.pm line 67
    Mojo::UserAgent::start(Mojo::UserAgent=HASH(0x5ffda5031418), Mojo::Transaction::HTTP=HASH(0x5ffda5f6fba8)) called at /usr/share/perl5/Test/Mojo.pm line 400
    Test::Mojo::_request_ok(Test::Mojo=HASH(0x5ffda2da6970), Mojo::Transaction::HTTP=HASH(0x5ffda5f6fba8), "/mock/job/58b074fbf5e421e0d79df4a5d38ef2e970a49ff51d06bd52718"...) called at /usr/share/perl5/Test/Mojo.pm line 343
    Test::Mojo::_build_ok(Test::Mojo=HASH(0x5ffda2da6970), "GET", "/mock/job/58b074fbf5e421e0d79df4a5d38ef2e970a49ff51d06bd52718"...) called at /usr/share/perl5/Test/Mojo.pm line 131
    Test::Mojo::get_ok(Test::Mojo=HASH(0x5ffda2da6970), "/mock/job/58b074fbf5e421e0d79df4a5d38ef2e970a49ff51d06bd52718"...) called at mock.t line 24

I understand it comes from JSON::Validator, but why does it bite me only on the job endpoint?

1
  • That's a lot to digest. Make the example smaller and more targeted and you might have more luck getting people to look to it. You might even discover the porblem yourself in that process. Commented Nov 14 at 23:26

1 Answer 1

2

Edit: I got one step further. By changing the schema the behaviour changed:

x-header-location: &HEADER_LOCATION
  description: Job status URL
  schema:
    type: string
x-header-retry-after: &HEADER_RETRY_AFTER
  description: Query delay in seconds
  schema:
    oneOf:
      - type: integer
        minimum: 1
      - type: string

components:
  schemas:
    ErrorMessage:
      description: An error descriptopm
      example:
        customer_error: You did not provide a password
        message: 'Request error: Password is missing'
      properties:
        customer_error:
          description: Error for consumer
          type: string
        message:
          description: Technical error description
          type: string
      required:
        - message
      type: object
    Job:
      properties:
        code:
          type: integer
        completedAt:
          format: date-time
          nullable: true
          type: string
        createdAt:
          format: date-time
          type: string
        jobId:
          type: string
        result:
          $ref: '#/components/schemas/Result'
        status:
          enum:
            - initiated
            - pending
            - completed
            - failed
          type: string
      required:
        - jobId
        - code
        - status
        - createdAt
      type: object
    JobAccepted:
      example:
        jobId: job-12345
        statusUrl: /jobs/job-12345
      properties:
        jobId:
          type: string
        statusUrl:
          type: string
      type: object
    Notification:
      properties:
        message:
          description: Beschreibung der Benachrichtigung
          type: string
        notificationID:
          description: ein eindeutiger Bezeichner dieser Benachrichtigung
          type: string
        path:
          description: Aktuell unbenutzt
          type: string
        severity:
          description: Einschätzung der Benachrichtigung
          enum:
            - CRITICAL
            - ERROR
            - WARNING
            - INFO
          type: string
      required:
        - notificationID
        - severity
        - message
    PayloadBaseObject:
      properties:
        verb:
          type: string
      required:
        - verb
      type: object
    Result:
      properties:
        notifications:
          items:
            $ref: '#/components/schemas/Notification'
          type: array
        payload:
          items:
            discriminator:
              propertyName: entityType
            oneOf:
              - $ref: '#/components/schemas/SyncMock'
          type: array
      type: object
    SyncMock:
      allOf:
        - $ref: '#/components/schemas/PayloadBaseObject'
        - properties:
            entityType:
              enum:
                - mock-details
              type: string
            reqkey:
              type: string
            result:
              properties:
                async:
                  description: demo
                  type: integer
              type: object
          required:
            - entityType
            - reqkey
          type: object
      description: Demo of a synchronuous API call
  securitySchemes:
    http:
      description: Basic Auth credentials
      scheme: basic
      type: http
info:
  title: mock service
  version: 0.0.1
openapi: 3.0.4
paths:
  /sync:
    get:
      description: Makes a synchronuous API call and waits for the result. There is no timeout.
      operationId: getMockSync
      responses:
        200:
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Job'
          description: Synchronuous job response
      security:
        - http: []
      summary: Mocks a synchronuous REST call
      tags:
        - mock
      x-mojo-to: Mock#sync
  /async:
    get:
      description: Makes an asynchronuous API call and does NOT wait for the result.
      operationId: getMockAsync
      responses:
        202:
          description: Asynchronuous job response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobAccepted'
          headers:
            Location: *HEADER_LOCATION
            Retry-after: *HEADER_RETRY_AFTER
      security:
        - http: []
      summary: Mocks an asynchronuous REST call
      tags:
        - mock
      x-mojo-to: Mock#async
  '/job/{jobid}':
    get:
      summary: Get job state
      description: Get job state
      operationId: retrieveJobStatus
      parameters:
        - description: A job ID
          in: path
          name: jobid
          required: true
          schema:
            type: string
      responses:
        200:
          description: 'Job done'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Job'
        202:
          description: 'Job not done yet'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobAccepted'
          headers:
            Location: *HEADER_LOCATION
            Retry-after: *HEADER_RETRY_AFTER
        404:
          description: 'Job not found (anymore)'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorMessage'
      x-mojo-to: Job#status
security:
  - http: []
servers:
  - description: Mock APIv2 service
    url: /mock
tags:
  - description: Just mocking
    name: mock

I resolved the header reference manually (x-header-* anchors) and also added these headers to metho. This is the required action from the error message. Now I see

[trace] OpenAPI >>> GET /mock/async [{"message":"Expected string - got Mojo::URL.","path":"\/Location"},{"message":"All of the oneOf rules match.","path":"\/Retry-after"}]
...
[trace] OpenAPI >>> GET /mock/job/58b074fbf5e421e0d79df4a5d38ef2e970a49ff51d06bd52718bacec483007f2 [{"message":"Expected string - got Mojo::URL.","path":"\/Location"},{"message":"All of the oneOf rules match.","path":"\/Retry-after"}]

This is also consistent with the formerly passing GET /mock/async. So maybe only my description is wrong.

Edit #2: Reading {"message":"Expected string - got Mojo::URL.","path":"\/Location"} I changed

                my $r
                    = $c->url_for(q(retrieveJobStatus) => { jobid => $jobid })
                    ->to_abs;

to

                my $r
                    = $c->url_for(q(retrieveJobStatus) => { jobid => $jobid })
                    ->to_abs->to_string;

and this error is gone.

Finally: The resolution is to change the oneOf of x-header-retry-after to anyOf and all problems are gone.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.