apple / swift-openapi-generator

Generate Swift client and server code from an OpenAPI document.
https://swiftpackageindex.com/apple/swift-openapi-generator/documentation
Apache License 2.0
1.21k stars 87 forks source link

How to calculate response length? #526

Open tib opened 3 months ago

tib commented 3 months ago

Question

Hello,

I'm currently trying to implement a HEAD endpoint, using the Swift OpenAPI generator & runtime. I'm using the following snippet, but I'm aware that it's far from ideal:

package func head(
    _ input: Operations.head.Input
) async throws -> Operations.head.Output {
    let result = MyCodableJSONObject()

    /// NOTE1: is there an other way to calculate this?
    var headerFields = HTTPFields()
    let res =
        try OpenAPIRuntime.Converter(
            configuration: .init()
        )
        .setResponseBodyAsJSON(
            result,
            headerFields: &headerFields,
            contentType: ""
        )

    var length: Int64 = 0
    switch res.length {
    case .known(let value):
        length = value
    case .unknown:
        break
    }
    /// NOTE2: int64 -> int conversion is not good...
    return .ok(.init(headers: .init(Content_hyphen_Length: Int(length))))
}

Is there a better way to calculate the Content-Length header for HEAD requests? 🤔

Many thanks.

Tib

czechboy0 commented 3 months ago

Hi @tib,

Converter, setResponseBodyAsJSON, are SPIs that you shouldn't be calling directly. They're there only for the generated code to call.

Can you describe what you're trying to do first, and we can help you achieve it without using SPIs, which are not guaranteed to be stable?

tib commented 3 months ago

I'm simply trying to implement a HEAD response and return the Content-Length.

https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/HEAD

I'm using a JSON response for the GET query (MyCodableJSONObject) and I'm trying to calculate the length of that response, so I can use the number in my HEAD handler.

I was not able to find a corresponding calculate method that I could use for this purpose.

I'm looking for a solution something like this (maybe a bit more generic, since this only works with JSON responses):

import HTTPTypes
@_spi(Generated) import OpenAPIRuntime

extension APIGateway {

    func calculateContentLength<T: Encodable>(_ value: T) throws -> Int64 {
        var headerFields = HTTPFields()
        let res =
            try OpenAPIRuntime.Converter(
                configuration: .init()
            )
            .setResponseBodyAsJSON(
                value,
                headerFields: &headerFields,
                contentType: ""
            )

        switch res.length {
        case .known(let value):
            return value
        case .unknown:
            return 0
        }
    }
}
czechboy0 commented 3 months ago

Could you share the OpenAPI definition for the operation you're implementing?

From the top of my head, a ServerMiddleware that throws away response bodies should do the trick, and in it you can verify that the content-length is present (it should be there already: https://github.com/apple/swift-openapi-runtime/blob/76951d77a0609599d2dc233e7e40808a74767c6a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift#L604).

That said, HEAD request support might be something we need to add first class support for. Would that help you here?

czechboy0 commented 1 month ago

Hi @tib, are you still interested in this? I think we should consider adding first class support for HEAD requests.

tib commented 1 month ago

Yes, this is still an issue for us. 👍

czechboy0 commented 1 month ago

@tib Can you share the OpenAPI doc (or at least the snippet) that includes the operation you'd like to implement HEAD for? Just to make sure we focus on the right example.

tib commented 1 month ago
openapi: 3.1.0
info:
  title: Example API
  description: 'Example'
  contact:
    name: Binary Birds
    url: https://binarybirds.com
    email: info@binarybirds.com
  version: 1.0.0
tags:
- name: Example
  description: ''
servers:
- url: http://localhost:8080
  description: dev
paths:
  /example:
    head:
      tags:
      - Example
      summary: Example head
      description: Example head request
      operationId: headOperation
      responses:
        200:
          $ref: '#/components/responses/ExampleResponse'

components:
  schemas:
    ExampleContentLength:
      type: integer
      description: Content length

  responses:
    ExampleResponse:
      description: Ok
      headers:
        Content-Length:
          $ref: '#/components/headers/Content-Length'
  headers:
    Content-Length:
      schema:
        $ref: '#/components/schemas/ExampleContentLength'
      description: Content length header
czechboy0 commented 1 month ago

Interesting, so what's the reason to include $ref: '#/components/responses/ExampleResponse' in the 200 response, if it'll never get returned, because it's a HEAD request? Should it be a GET instead, and the ask here would be that all GET requests also support HEAD requests? (Maybe that should be done by the concrete transport, not sure.)