vapor / websocket-kit

WebSocket client library built on SwiftNIO
https://docs.vapor.codes/4.0/advanced/websockets/
MIT License
272 stars 79 forks source link

Support needed when using WebSocket Kit as client #125

Closed DanielMandea closed 1 year ago

DanielMandea commented 1 year ago

Describe the bug

Inside one of my routes, I need to call a certain Socket API and then use the response and respond back in that certain route. Initially, I used URLSession and webSocketTask and all worked fine, but when building the Dockerfile I realized that webSocketTask is not yet supported on Linux. The next step was to try WebSocketKit but unfortunately, I have some issues to make it work. Below I will add a few details related to my issue.

My implementation:

        let promise = elg.next().makePromise(of: String.self)
        try await WebSocket.connect(to: url, headers: headers, on: elg) { ws in
            do {
                if let body {
                    try await ws.send(body)
                }
                ws.onText { ws, string in
                    promise.succeed(string)
                    do {
                        try await ws.close()
                    } catch {
                        promise.fail(error)
                    }
                }
            } catch {
                promise.fail(error)
            }
        }

        let result = try await promise.futureResult.get()
        guard let data = result.data(using: .utf8) else {throw Abort(.expectationFailed)}
        return try decoder.decode(T.self, from: data)

elg is provided as a function parameter and ist's actually provided as follows elg = req.application.eventLoopGroup url contains certain ws URL

The code above is actually embedded inside a function that returns async throws -> T

The first issue that I have is related to the Promise:

App/WebSocketXYZ.swift:75: Fatal error: leaking promise created at (file: "App/WebSocketXYZ.swift", line: 75)

Not sure what is the issue and if I've used Promise the wrong way

If I remove the promises and only test if I can connect to the ws URL i get the following error:

invalidResponseStatus(HTTPResponseHead { version: HTTP/1.1, status: 403 Forbidden, headers: [(\"Content-Type\", \"application/json; charset=UTF-8\"), (\"Content-Length\", \"150\"), (\"Connection\", \"close\"), (\"Date\", \"Tue, 31 Jan 2023 10:01:54 GMT\"), (\"x-amz-apigw-id\", \"fmkK3E-OliAFjSw=\"), (\"X-Cache\", \"Error from cloudfront\"), (\"Via\", \"1.1 6f5ac69c39e434663876b6bbf4ccb97e.cloudfront.net (CloudFront)\"), (\"X-Amz-Cf-Pop\", \"OTP50-C1\"), (\"X-Amz-Cf-Id\", \"wkzowry6dvUMsMT0Gr_yV1sisiwrk1WkZUMbAcJlplC70g-QR9Homg==\")] })

A clear and concise description of what the bug is.

There are two issues that i have:

  1. I am not able to properly use the promise in order to return success or failure;
  2. I am not able to connect to ws API via WebSocketKit

Please give me a hint on how i can move forward.

To Reproduce

I've just used the try await WebSocket.connect(to: as described above and seen under unit tests of this repo.

Steps to reproduce the behavior:

  1. Add package with configuration '...'
  2. Send request with options '...'
  3. See error

Expected behavior

A clear and concise description of what you expected to happen.

Environment

Nio

{
      "identity" : "swift-nio",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/apple/swift-nio.git",
      "state" : {
          "revision" : "7e3b50b38e4e66f31db6cf4a784c6af148bac846",
          "version" : "2.46.0"
       }
},

Vapor

{
      "identity" : "vapor",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/vapor/vapor.git",
      "state" : {
        "revision" : "888c8b68642c1d340b6b3e9b2b8445fb0f6148c9",
        "version" : "4.68.0"
      }
},

WebSocketKit

{
      "identity" : "websocket-kit",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/vapor/websocket-kit.git",
      "state" : {
        "revision" : "2d9d2188a08eef4a869d368daab21b3c08510991",
        "version" : "2.6.1"
      }
}

Additional context

Add any other context about the problem here.

MahdiBM commented 1 year ago

You must succeed/fail a promise once, and you must do it only once. Your code doesn't take that into account. A better code would be:

do {
    if let body {
        try await ws.send(body)
    } else {
        promise.fail(SOME_ERROR_CANT_FIND_BODY) /// or even better, check for body before making the ws connection
    }
    ws.onText { ws, string in
        promise.succeed(string)
        _ = try? await ws.close() /// Try to close the ws, but don't care about the result.

        /// do {
        ///      try? await ws.close()
        /// } catch {
        ///     promise.fail(error) CANT do this. you have a chance to complete the promise twice.
        /// }
    }
} catch {
    promise.fail(error)
}

The second error which is the mani error, says that your websocket connection is forbidden. I would guess you're not sending "good" headers, or for some reason the ws server doesn't like you.

DanielMandea commented 1 year ago

Thanks @MahdiBM for your support. Related to the promise issue I've managed to fix it. I've also managed to advance with the main problem websocket connection is forbidden, in this case, I was missing some headers like:

        "Connection":"Upgrade",
        "Upgrade": "websocket",
        "Sec-WebSocket-Version": "13",
        "Sec-WebSocket-Extensions": "permessage-deflate; client_max_window_bits",

Now I am able to connect to the socket and send a message but the response that I get is the following:

"lTmo�0\u{10}�+���\u{01}\u{12}\u{02}�H�\u{1A}^֢�\u{17}�I��ɉ\u{0F}�p��vZ���}�Rh�\u{07}����rw�c^\u{08}<����B�@\u{19}h�d5��f�+i�y\'TJ��\u{03}�\u{19}�\u{13}��-��ϲ��GYH}\u{1A}v2?��4l�8��=rC�*u\u{06}IQ\u{08}�QW�\u{15}�4\u{07},�\u{17}�8U)\u{17}.��{e���+�zC2\r�\u{02}K\u{1C}\u{14}�¶�\n�Vg\u{1D}�����\t\u{1B}a����gH��\u{0E}`\'��m\u{11}��Y��Ɠ���|=%���!\u{15}b@��F\u{0B}��[[�~�I\u{0B}�siAK*\u{1A}\0y�p�H�<4��J��T�\u{05}���?��]����2��O���\u{15}���o�K�V*K!nHU?��*]u�h\u{05}=\nE�[�*@�ws>\u{0C}\u{15}�j�\u{16}\u{0E}qF�`\u{0C}ݹ���2<lK�i���\u{13}\u{15}��zu�^ձ�<�p4�˭rzXjKT��\u{13}ݵ��\u{14}8\r����#��\u{1C}\u{13}\'�\u{17}�V؍�(p�^&L$�O��TT��V�\u{08}L��)��7d˵���ګd�̒�C���gx�܌&���|�8S)B�-:��������\u{0B}�9�b\u{04}\u{16}��\u{0F}����B���\u{11}\u{12}s4\u{0C}��tk�8���ً���]8\u{10}\'M�\\�K\u{14}�A؎:ݸw��@˜v;�a��#�r�W�S�z���|:�3�9\u{1B}/q�:w},��3�,����=��K\u{08}0p��z\u{03}�2;0�W���]���@WH��TF�����x=^\u{0E}������8�w��\\V�����\\�q����MV��\u{1B}���y߹�1��,s\u{1C}�����{\u{1C}0�+er�\rN^�AP�+O�\u{04}��\u{0F}ދ�J\u{7F}*\\�\'����~����\u{0B}As����j�yN��\u{02}�^Ғ\u{0B}��\u{1D}����*n��\u{18}�\u{01}쇺�۶�\n!`Q�\u{13}P�^�\u{1B}���/��\u{16}��I�V�nO^��?\0"

I've tried to connect to the same ws service and all is working fine. I still guess that I am missing some other headers like Sec-WebSocket-Key and Host Which are the only ones that postman adds automatically with <calculated at runtime> value and I don't .

DanielMandea commented 1 year ago

The main issue for websocket connection is forbidden was related to the fact that I was adding some custom headers that I was adding.