Skip to content

Deal with different success responses #119

Closed

Description

@schlichtanders

The new version 1.x is no longer dealing with different success responses.

I had one response type corresponding to 202 and one to 200, however after migrating to openapi 1.x it fails when the type of the 202 is returned.

It searches for a success response, and finds the wrong one for 200, and then fails with a schema validation failed response, which makes sense as it compared the wrong schemas.

How is one supposed to return the different return types if not directly via return? is there a way to give the status 202 together with the return type?


Similarly, I also have the case where I would like to return a ClientError response in the 400 realm. When I look at the code in _server_response, also this probably will fail, looking for a 200 schema without actually checking the type, which clearly corresponds to 400 type.

EDIT: A suggestion

instead of the _success_response function, I expected rather something like the following:

    function _response_by_type(responses, result)
        # first try to find type
        for response in responses
            for media in response.media
                if media.type == typeof(result)
                    return response
                end
            end
        end
        # fall back to default selector
        for response in responses
            selector = uppercase(response.selector)
            if selector == "DEFAULT"
                return response
            end
        end
        # fall back to nothing
        return nothing
    end

Activity

  1. schlichtanders commented on Sep 17, 2026

    @schlichtanders
    Author

    Here my current full workaround when including the OpenAPI generated Server. Works

    _replace_success_response(ex) = ex
    function _replace_success_response(ex::Expr)
        # _success_response(operation.responses) -> _response_by_type(operation.responses, result)
        if ex.head == :call && ex.args[1] == :_success_response
            return Expr(:call, :_response_by_type, ex.args[2], :result)
        end
        # Recursively process children
        return Expr(ex.head, map(_replace_success_response, ex.args)...)
    end
    include(_replace_success_response, "../../openapi/APIServer.jl")
    @eval APIServer begin
        function _response_by_type(responses, result)
            # first try to find type
            for response in responses
                for media in response.media
                    if media.type == typeof(result)
                        return response
                    end
                end
            end
            # fall back to default selector
            for response in responses
                selector = uppercase(response.selector)
                if selector == "DEFAULT"
                    return response
                end
            end
            # fall back to nothing
            return nothing
        end
    end

    Is a pullrequest welcome?

  2. tanmaykm commented on Sep 19, 2026

    @tanmaykm
    Member

    Thanks for the clear report and the workaround. Yes, a PR is welcome. Some notes first so it lands in a shape we can keep.

    What happens today

    The generated server always picks the first documented 2xx response and encodes the returned value against it. So with

    responses:
      "200":
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Done" }
      "202":
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Accepted" }

    a handler that returns an Accepted is checked against the Done schema, and validation fails. That is what you are seeing.

    This is not new in 1.x. The 0.2 server did the same thing: any plain return value went out as a 200. What changed is that 1.x now validates the response body before sending it, so the mismatch shows up as an error instead of a 200 with the wrong body. We hit the same thing when porting our own services, and the answer so far has been the escape hatch that already exists: return an HTTP.Response yourself.

    function submit(request, body)
        if needs_processing(body)
            return HTTP.Response(202, ["Content-Type" => "application/json"], JSON.json(Accepted(...)))
        end
        return Done(...)   # encoded and validated as the 200
    end

    That works, but you lose validation for that branch and have to encode the body by hand. So we agree this deserves a proper fix.

    Why picking by type is not enough

    The _response_by_type idea works for your document, but it falls over in common cases:

    • Two statuses often document the same type. 200 and 201 both returning a Widget is normal.
    • Many responses are plain strings, byte arrays or free-form objects, so there is no type to tell them apart.
    • A handler returning a Dict or a Vector matches nothing, and the fallback to default would then send an error-shaped body as the status of the default response.

    So the status has to come from the handler, not be guessed from the value.

    Suggested shape

    Let the handler say the status explicitly, and keep everything else the same:

    function submit(request, body)
        if needs_processing(body)
            return OpenAPI.Reply(202, Accepted(...))   # name open for discussion
        end
        return Done(...)                                # plain value still means "first 2xx"
    end
    
    function get_item(request, id)
        item = lookup(id)
        item === nothing && return OpenAPI.Reply(404, ClientError(message = "no such item"))
        return item
    end

    _server_response then looks up the response for that status (exact code first, then 4XX-style ranges, then default, which the runtime already does for clients in _select_response), validates the value against that response's schema, and sends it with that status. A plain value keeps today's behaviour, so existing servers do not change. Returning an HTTP.Response stays as the escape hatch for anything else.

    This covers both of your cases, the 202 and the 400, with one mechanism, and the body is still validated.

    What the PR would need

    • The wrapper type in the runtime and the status lookup in the generated _server_response.
    • A clear error when the status is not documented for that operation.
    • A round-trip test in test/servergen.jl with a 200/202 pair and a 4xx typed error.
    • A short section in docs/src/servers.md with an example like the one above.

    One more thing worth fixing on the way: the generated handler signature lists the union of all documented body types as the return type. That reads as if the server dispatches on the type, which it does not, and it is probably part of why the type-based fix looked natural. Happy to discuss the wrapper's name and whether a (status, value) tuple would be nicer than a struct.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions