fix: disable websocket compression for startup logs in Safari (#8087)

This commit is contained in:
Mathias Fredriksson
2023-06-20 16:29:32 +03:00
committed by GitHub
parent c3781d95b4
commit b8ba287128
5 changed files with 48 additions and 8 deletions

6
coderd/apidoc/docs.go generated
View File

@ -4599,6 +4599,12 @@ const docTemplate = `{
"description": "Follow log stream", "description": "Follow log stream",
"name": "follow", "name": "follow",
"in": "query" "in": "query"
},
{
"type": "boolean",
"description": "Disable compression for WebSocket connection",
"name": "no_compression",
"in": "query"
} }
], ],
"responses": { "responses": {

View File

@ -4049,6 +4049,12 @@
"description": "Follow log stream", "description": "Follow log stream",
"name": "follow", "name": "follow",
"in": "query" "in": "query"
},
{
"type": "boolean",
"description": "Disable compression for WebSocket connection",
"name": "no_compression",
"in": "query"
} }
], ],
"responses": { "responses": {

View File

@ -405,6 +405,7 @@ func (api *API) patchWorkspaceAgentStartupLogs(rw http.ResponseWriter, r *http.R
// @Param before query int false "Before log id" // @Param before query int false "Before log id"
// @Param after query int false "After log id" // @Param after query int false "After log id"
// @Param follow query bool false "Follow log stream" // @Param follow query bool false "Follow log stream"
// @Param no_compression query bool false "Disable compression for WebSocket connection"
// @Success 200 {array} codersdk.WorkspaceAgentStartupLog // @Success 200 {array} codersdk.WorkspaceAgentStartupLog
// @Router /workspaceagents/{workspaceagent}/startup-logs [get] // @Router /workspaceagents/{workspaceagent}/startup-logs [get]
func (api *API) workspaceAgentStartupLogs(rw http.ResponseWriter, r *http.Request) { func (api *API) workspaceAgentStartupLogs(rw http.ResponseWriter, r *http.Request) {
@ -415,6 +416,7 @@ func (api *API) workspaceAgentStartupLogs(rw http.ResponseWriter, r *http.Reques
logger = api.Logger.With(slog.F("workspace_agent_id", workspaceAgent.ID)) logger = api.Logger.With(slog.F("workspace_agent_id", workspaceAgent.ID))
follow = r.URL.Query().Has("follow") follow = r.URL.Query().Has("follow")
afterRaw = r.URL.Query().Get("after") afterRaw = r.URL.Query().Get("after")
noCompression = r.URL.Query().Has("no_compression")
) )
var after int64 var after int64
@ -460,7 +462,21 @@ func (api *API) workspaceAgentStartupLogs(rw http.ResponseWriter, r *http.Reques
api.WebsocketWaitGroup.Add(1) api.WebsocketWaitGroup.Add(1)
api.WebsocketWaitMutex.Unlock() api.WebsocketWaitMutex.Unlock()
defer api.WebsocketWaitGroup.Done() defer api.WebsocketWaitGroup.Done()
conn, err := websocket.Accept(rw, r, nil)
opts := &websocket.AcceptOptions{}
// Allow client to request no compression. This is useful for buggy
// clients or if there's a client/server incompatibility. This is
// needed with e.g. nhooyr/websocket and Safari (confirmed in 16.5).
//
// See:
// * https://github.com/nhooyr/websocket/issues/218
// * https://github.com/gobwas/ws/issues/169
if noCompression {
opts.CompressionMode = websocket.CompressionDisabled
}
conn, err := websocket.Accept(rw, r, opts)
if err != nil { if err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Failed to accept websocket.", Message: "Failed to accept websocket.",

View File

@ -689,12 +689,13 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent}/sta
### Parameters ### Parameters
| Name | In | Type | Required | Description | | Name | In | Type | Required | Description |
| ---------------- | ----- | ------------ | -------- | ------------------ | | ---------------- | ----- | ------------ | -------- | -------------------------------------------- |
| `workspaceagent` | path | string(uuid) | true | Workspace agent ID | | `workspaceagent` | path | string(uuid) | true | Workspace agent ID |
| `before` | query | integer | false | Before log id | | `before` | query | integer | false | Before log id |
| `after` | query | integer | false | After log id | | `after` | query | integer | false | After log id |
| `follow` | query | boolean | false | Follow log stream | | `follow` | query | boolean | false | Follow log stream |
| `no_compression` | query | boolean | false | Disable compression for WebSocket connection |
### Example responses ### Example responses

View File

@ -4,6 +4,7 @@ import * as Types from "./types"
import { DeploymentConfig } from "./types" import { DeploymentConfig } from "./types"
import * as TypesGen from "./typesGenerated" import * as TypesGen from "./typesGenerated"
import { delay } from "utils/delay" import { delay } from "utils/delay"
import userAgentParser from "ua-parser-js"
// Adds 304 for the default axios validateStatus function // Adds 304 for the default axios validateStatus function
// https://github.com/axios/axios#handling-errors Check status here // https://github.com/axios/axios#handling-errors Check status here
@ -1231,9 +1232,19 @@ export const watchStartupLogs = (
agentId: string, agentId: string,
{ after, onMessage, onDone, onError }: WatchStartupLogsOptions, { after, onMessage, onDone, onError }: WatchStartupLogsOptions,
) => { ) => {
// WebSocket compression in Safari (confirmed in 16.5) is broken when
// the server sends large messages. The following error is seen:
//
// WebSocket connection to 'wss://.../startup-logs?follow&after=0' failed: The operation couldnt be completed. Protocol error
//
const noCompression =
userAgentParser(navigator.userAgent).browser.name === "Safari"
? "&no_compression"
: ""
const proto = location.protocol === "https:" ? "wss:" : "ws:" const proto = location.protocol === "https:" ? "wss:" : "ws:"
const socket = new WebSocket( const socket = new WebSocket(
`${proto}//${location.host}/api/v2/workspaceagents/${agentId}/startup-logs?follow&after=${after}`, `${proto}//${location.host}/api/v2/workspaceagents/${agentId}/startup-logs?follow&after=${after}${noCompression}`,
) )
socket.binaryType = "blob" socket.binaryType = "blob"
socket.addEventListener("message", (event) => { socket.addEventListener("message", (event) => {