vapor / vapor

💧 A server-side Swift HTTP web framework.
https://vapor.codes
MIT License
24.56k stars 1.45k forks source link

Seems like Client Post requests will never send a `body` with the request #3245

Closed AndrewBoryk closed 2 weeks ago

AndrewBoryk commented 2 weeks ago

https://github.com/vapor/vapor/blob/1310c6f5462e23bca08fcf7c26448891a4908294/Sources/Vapor/Client/Client.swift#L67

I'm attempting to make a POST request to an external API. When making the post request on the client, I was noticing that I'd get an error message back that I wasn't sending the required fields in the request body.

Then tracing the request, I noticed this line was always sending body as nil, and so the content that is being encoded in the request is never getting forwarded along.

AndrewBoryk commented 2 weeks ago

Here is an example of what I am attempting to send:

let payload = TwitchTokenPayload(client_id: REDACTED,
                                                           client_secret: REDACTED,
                                                           grant_type: "client_credentials")

let response = try? await req.client.post("https://id.twitch.tv/oauth2/token", content: payload)

And seems like the body being sent in the request is always empty

0xTim commented 2 weeks ago

@AndrewBoryk you can use the beforeSend closure:

let response = try? await req.client.post("https://id.twitch.tv/oauth2/token", content: payload) {  outgoingReq in
  try outgoingReq.content.encode(payload)
}
AndrewBoryk commented 2 weeks ago

@0xTim Yup, I tried that as well, that still wouldn't send as content is not passed through to the ClientRequest

0xTim commented 2 weeks ago

It is, on line 69 of the code you linked above

AndrewBoryk commented 2 weeks ago

Actually yes, sorry you are correct, although I still attempted that for it to not work

AndrewBoryk commented 2 weeks ago

@0xTim Still no dice.

Screenshot 2024-10-23 at 10 10 58 PM

AndrewBoryk commented 2 weeks ago

For context TwitchTokenPayload:

struct TwitchTokenPayload: Content {

    let client_id: String
    let client_secret: String
    let grant_type: String
}
AndrewBoryk commented 2 weeks ago

Is there a quick method for me to be able to log all outbound requests from the client?

0xTim commented 2 weeks ago

How about a breakpoint in line 73 from the link above and printing the request

AndrewBoryk commented 2 weeks ago

I decoded and logged the outboundRequest.content right after, and all the data I intended on sending is in there.

Screenshot 2024-10-23 at 10 28 47 PM

Went a little deeper and I see that some body is present, but not sure if it is getting lost along the way. Would be odd to think that it would be? Not sure the best method for decoding in console

AndrewBoryk commented 2 weeks ago

TwitchTokenPayload(client_id: "REDACTED", client_secret: "REDACTED", grant_type: "client_credentials")

This is me logging the decoded content.

Is there a place where I could log the data in the body before it is sent? Or a curl for the request? Maybe something is getting injected into body, but somewhere along the way it becomes malformed. Hard to debug without being able to peer into the contents. Maybe something to do with buffer capacity? And data maybe getting cut off?

AndrewBoryk commented 2 weeks ago

Okay, I was able to get it to work. It took a whole bunch of playing around on Postman to recreate the issue.

So apparently the .json format was not being received by twitch as json format, instead of was being received as plain text. Not sure why. Changing the format to .urlEncodedForm worked.

mkll commented 2 weeks ago

So apparently the .json format was not being received by twitch as json format, instead of was being received as plain text. Not sure why. Changing the format to .urlEncodedForm worked.

Try to provide your own HTTP headers along with request, it may help:

private var headers: HTTPHeaders {
    var headers = HTTPHeaders()
    headers.add(name: .contentType, value: "application/json")
    return headers
}

Usage:

let response = try await req.client.send(.POST, headers: headers, to: <URI>) { outgoingReq in
    try outgoingReq.content.encode(payload, as: .json)
}
mkll commented 2 weeks ago

@AndrewBoryk Also, you may use the following helper functions:

func printHdrs(req: Vapor.Request) {
    print(req.headers.map { $0 }.map { "\($0.name): \($0.value)\n" }.joined())
}

func printClientHdrs(req: Vapor.ClientRequest) {
    print(req.headers.map { $0 }.map { "\($0.name): \($0.value)\n" }.joined())
}

func printReq(req: Vapor.Request) {
    let tpl = """
    == <REQUEST> ======================
    \(req.description)
    == </REQUEST> =====================
    """
    print(tpl)
}

func printClientReq(req: Vapor.ClientRequest) {
    let byteBufferString = req.body?.string ?? ""

    print("== <CLIENT_REQUEST> ===============")
    printClientHdrs(req: req)
    print("\n", byteBufferString)
    print("== </CLIENT_REQUEST> ==============")
}

func printRes(res: Vapor.Response) {
    let tpl = """
    == <RESPONSE> =====================
    \(res.description)
    == </RESPONSE> ====================
    """
    print(tpl)
}

func printClientRes(res: Vapor.ClientResponse) {
    let tpl = """
    == <CLIENT_RESPONSE> ==============
    \(res.description)
    == </CLIENT_RESPONSE> =============
    """
    print(tpl)
}
0xTim commented 2 weeks ago

Assuming you're using Content then that will add the application/json headers. Given this is an OAuth endpoint I expect Twitch requires it to be URL Form Encoded. You can set this with:

struct TwitchTokenPayload: Content {
    static let defaultContentType = .urlEncodedForm
    let client_id: String
    let client_secret: String
    let grant_type: String
}

Vapor will encode that correctly and set the correct headers

0xTim commented 2 weeks ago

I'm going to close this now as I believe it's fixed. Feel free to reopen if it's still an issue!

AndrewBoryk commented 2 weeks ago

I was able to reproduce by removing Content-Type from the headers in postman. I am not sure why Content-Type would not be sent correctly, maybe some strange behavior with how headers inject?

AndrewBoryk commented 2 weeks ago

Just was strange to me that with Content-Type and setting for it to encode as .json that it wouldn't do so... very strange. Idk if there is something worth looking at here

AndrewBoryk commented 2 weeks ago

Strange that it worked as json in Postman, maybe Postman is doing some magic under the hood