swift-openapi-generator icon indicating copy to clipboard operation
swift-openapi-generator copied to clipboard

How to calculate response length?

Open tib opened this issue 1 year ago • 8 comments

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

tib avatar Feb 15 '24 10:02 tib

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?

czechboy0 avatar Feb 15 '24 10:02 czechboy0

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
        }
    }
}

tib avatar Feb 15 '24 11:02 tib

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 avatar Feb 15 '24 13:02 czechboy0

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

czechboy0 avatar Apr 16 '24 10:04 czechboy0

Yes, this is still an issue for us. 👍

tib avatar Apr 16 '24 10:04 tib

@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.

czechboy0 avatar Apr 16 '24 11:04 czechboy0

openapi: 3.1.0
info:
  title: Example API
  description: 'Example'
  contact:
    name: Binary Birds
    url: https://binarybirds.com
    email: [email protected]
  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

tib avatar Apr 18 '24 13:04 tib

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.)

czechboy0 avatar Apr 18 '24 16:04 czechboy0

@adam-fowler @joannis @0xTim What's the current handling of HEAD requests in Vapor and Hummingbird? Is this something that should be handled by the transport?

czechboy0 avatar Oct 29 '24 09:10 czechboy0

You can add a flag to the Hummingbird router to auto generate head endpoints (It'll run endpoint but don't return body).

adam-fowler avatar Oct 29 '24 09:10 adam-fowler

Great - if Vapor has something similar, then I'd prefer to leave this to the transports to handle.

czechboy0 avatar Oct 29 '24 10:10 czechboy0