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

Allowing disabling percent encoding for some HTTP header fields

Open zunda-pixel opened this issue 1 year ago • 10 comments

Question

I want to redirect to domain2.com from domain1.com.

I can redirect same domain. ex: fromdomain1.com/login to domain1.com/account.

responses:
  '303':
    headers:
      location:
        schema:
          type: string
func login(_ input: Operations.login.Input) async throws -> Operations.login.Output {
  // login
  return .seeOther(.init(headers: .init(location: "https:/example2.com")))
}

zunda-pixel avatar Feb 17 '24 14:02 zunda-pixel

Hi @zunda-pixel,

just to confirm, you're writing a server here, right? You can return a completely arbitrary URL in the response's Location header, can you clarify why you say you can't redirect to a different domain?

czechboy0 avatar Feb 17 '24 15:02 czechboy0

Yes, I am writing a server.

Base url is keeping...

http://127.0.0.1:8080/login -> http://127.0.0.1:8080/https%3A%2F%2Fexample2.com

zunda-pixel avatar Feb 17 '24 16:02 zunda-pixel

Hmm, baseURL is only a client-side concept in Swift OpenAPI Generator, not a server one. Can you describe in detail how you're testing this and how you're getting this URL?

czechboy0 avatar Feb 17 '24 20:02 czechboy0

This is sample repository that has issue I told. Please check. https://github.com/zunda-pixel/LoginServer

openapi: '3.1.0'
info:
  title: LoginService
  version: 1.0.0
servers:
  - url: https://example.com/
    description: Example service deployment.
paths:
  /login:
    get:
      operationId: login
      responses:
        '303':
          description: A success response Login
          headers:
            location:
              schema:
                type: string
import OpenAPIRuntime
import OpenAPIVapor
import Vapor

struct Handler: APIProtocol {
  func login(_ input: Operations.login.Input) async throws -> Operations.login.Output {
    return .seeOther(.init(headers: .init(location: "https://apple.com")))
  }
}

@main struct LoginServer {
  static func main() async throws {
    let app = Vapor.Application()
    let transport = VaporTransport(routesBuilder: app)
    let handler = Handler()
    try handler.registerHandlers(on: transport)
    try await app.execute()
  }
}

zunda-pixel avatar Feb 18 '24 04:02 zunda-pixel

Can you clarify what the issue is? The code in the project all looks correct.

What are the steps you're taking, what is the result you see, and what is the result you expect? That'll help us understand where the mismatch is.

czechboy0 avatar Feb 18 '24 07:02 czechboy0

Current Result

  1. http://localhost:8080/login
  2. http://127.0.0.1:8080/https%3A%2F%2Fapple.com

Expecting Result

  1. http://localhost:8080/login
  2. https://apple.com

zunda-pixel avatar Feb 18 '24 07:02 zunda-pixel

Which HTTP client are you using? A web browser? curl?

czechboy0 avatar Feb 18 '24 16:02 czechboy0

I use a web browser.

zunda-pixel avatar Feb 18 '24 16:02 zunda-pixel

Thank you @zunda-pixel, I was able to isolate the issue.

The problem is that OpenAPI-defined headers are serialized according to the rules of RFC6570 (details here), which dictate that non-reserved characters need to be percent encoded. However, in the Location header, that causes the URL in the response header to be percent-encoded, and the web browser client doesn't remove the percent encoding, it seems.

As a workaround, add a middleware that removes the percent encoding of the Location header:

import OpenAPIRuntime
import OpenAPIVapor
import Vapor
import HTTPTypes

struct Handler: APIProtocol {
  func login(_ input: Operations.login.Input) async throws -> Operations.login.Output {
    return .seeOther(.init(headers: .init(location: "https://apple.com")))
  }
}

@main struct LoginServer {
  static func main() async throws {
    let app = Vapor.Application()
    let transport = VaporTransport(routesBuilder: app)
    let handler = Handler()
    try handler.registerHandlers(on: transport, middlewares: [
        UnescapeLocationHeaderMiddleware()
    ])
    try await app.execute()
  }
}

struct UnescapeLocationHeaderMiddleware: ServerMiddleware {
    func intercept(
        _ request: HTTPRequest,
        body: HTTPBody?,
        metadata: ServerRequestMetadata,
        operationID: String,
        next: (HTTPRequest, HTTPBody?, ServerRequestMetadata) async throws -> (HTTPResponse, HTTPBody?)
    ) async throws -> (HTTPResponse, HTTPBody?) {
        var (response, responseBody) = try await next(request, body, metadata)
        guard let location = response.headerFields[.location] else {
            return (response, responseBody)
        }
        response.headerFields[.location] = location.removingPercentEncoding
        return (response, responseBody)
    }
}

Now, thanks for reporting this. It's a bit troubling, and I suspect we'll need some way to make this more compatible with clients that don't percent-decode header fields.

If you don't mind, I'll repurpose this issue to track improving Swift OpenAPI Generator this way and rename it.

czechboy0 avatar Feb 19 '24 13:02 czechboy0

Thank you for looking into this issue, and providing a workaround.

zunda-pixel avatar Feb 19 '24 14:02 zunda-pixel