Skip to content

Use Axios client for EventSource #440

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,43 @@

## Unreleased

- Remove agent singleton so that client TLS certificates are reloaded on every API request.

### Fixed

- Remove agent singleton so that client TLS certificates are reloaded on every API request.
- Use Axios client to receive event stream so TLS settings are properly applied.

## [v1.4.1](https://github.com/coder/vscode-coder/releases/tag/v1.4.1) (2025-02-19)

### Fixed

- Recreate REST client in spots where confirmStart may have waited indefinitely.

## [v1.4.0](https://github.com/coder/vscode-coder/releases/tag/v1.4.0) (2025-02-04)

### Fixed

- Recreate REST client after starting a workspace to ensure fresh TLS certificates.

### Changed

- Use `coder ssh` subcommand in place of `coder vscodessh`.

## [v1.3.10](https://github.com/coder/vscode-coder/releases/tag/v1.3.10) (2025-01-17)

### Fixed

- Fix bug where checking for overridden properties incorrectly converted host name pattern to regular expression.

## [v1.3.9](https://github.com/coder/vscode-coder/releases/tag/v1.3.9) (2024-12-12)

### Fixed

- Only show a login failure dialog for explicit logins (and not autologins).

## [v1.3.8](https://github.com/coder/vscode-coder/releases/tag/v1.3.8) (2024-12-06)

### Changed

- When starting a workspace, shell out to the Coder binary instead of making an
API call. This reduces drift between what the plugin does and the CLI does. As
part of this, the `session_token` file was renamed to `session` since that is
Expand Down
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -208,10 +208,10 @@
],
"menus": {
"commandPalette": [
{
"command": "coder.openFromSidebar",
"when": "false"
}
{
"command": "coder.openFromSidebar",
"when": "false"
}
],
"view/title": [
{
Expand Down Expand Up @@ -275,7 +275,7 @@
"test:ci": "CI=true yarn test"
},
"devDependencies": {
"@types/eventsource": "^1.1.15",
"@types/eventsource": "^3.0.0",
"@types/glob": "^7.1.3",
"@types/node": "^18.0.0",
"@types/node-forge": "^1.3.11",
Expand Down Expand Up @@ -309,7 +309,7 @@
"dependencies": {
"axios": "1.7.7",
"date-fns": "^3.6.0",
"eventsource": "^2.0.2",
"eventsource": "^3.0.5",
"find-process": "^1.4.7",
"jsonc-parser": "^3.3.1",
"memfs": "^4.9.3",
Expand Down
3 changes: 3 additions & 0 deletions src/api-helper.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { isApiError, isApiErrorResponse } from "coder/site/src/api/errors"
import { Workspace, WorkspaceAgent } from "coder/site/src/api/typesGenerated"
import { ErrorEvent } from "eventsource"
import { z } from "zod"

export function errToStr(error: unknown, def: string) {
Expand All @@ -9,6 +10,8 @@ export function errToStr(error: unknown, def: string) {
return error.response.data.message
} else if (isApiErrorResponse(error)) {
return error.message
} else if (error instanceof ErrorEvent) {
return error.code ? `${error.code}: ${error.message || def}` : error.message || def
} else if (typeof error === "string" && error.trim().length > 0) {
return error
}
Expand Down
56 changes: 56 additions & 0 deletions src/api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { AxiosInstance } from "axios"
import { spawn } from "child_process"
import { Api } from "coder/site/src/api/api"
import { ProvisionerJobLog, Workspace } from "coder/site/src/api/typesGenerated"
import { FetchLikeInit } from "eventsource"
import fs from "fs/promises"
import { ProxyAgent } from "proxy-agent"
import * as vscode from "vscode"
Expand Down Expand Up @@ -90,6 +92,58 @@ export async function makeCoderSdk(baseUrl: string, token: string | undefined, s
return restClient
}

/**
* Creates a fetch adapter using an Axios instance that returns streaming responses.
* This can be used with APIs that accept fetch-like interfaces.
*/
export function createStreamingFetchAdapter(axiosInstance: AxiosInstance) {
return async (url: string | URL, init?: FetchLikeInit) => {
const urlStr = url.toString()

const response = await axiosInstance.request({
url: urlStr,
headers: init?.headers as Record<string, string>,
responseType: "stream",
validateStatus: () => true, // Don't throw on any status code
})
const stream = new ReadableStream({
start(controller) {
response.data.on("data", (chunk: Buffer) => {
controller.enqueue(chunk)
})

response.data.on("end", () => {
controller.close()
})

response.data.on("error", (err: Error) => {
controller.error(err)
})
},

cancel() {
response.data.destroy()
return Promise.resolve()
},
})

return {
body: {
getReader: () => stream.getReader(),
},
url: urlStr,
status: response.status,
redirected: response.request.res.responseUrl !== urlStr,
headers: {
get: (name: string) => {
const value = response.headers[name.toLowerCase()]
return value === undefined ? null : String(value)
},
},
}
}
}

/**
* Start or update a workspace and return the updated workspace.
*/
Expand Down Expand Up @@ -182,6 +236,7 @@ export async function waitForBuild(
path += `&after=${logs[logs.length - 1].id}`
}

const agent = await createHttpAgent()
await new Promise<void>((resolve, reject) => {
try {
const baseUrl = new URL(baseUrlRaw)
Expand All @@ -194,6 +249,7 @@ export async function waitForBuild(
| undefined,
},
followRedirects: true,
agent: agent,
})
socket.binaryType = "nodebuffer"
socket.on("message", (data) => {
Expand Down
12 changes: 4 additions & 8 deletions src/workspaceMonitor.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Api } from "coder/site/src/api/api"
import { Workspace } from "coder/site/src/api/typesGenerated"
import { formatDistanceToNowStrict } from "date-fns"
import EventSource from "eventsource"
import { EventSource } from "eventsource"
import * as vscode from "vscode"
import { createStreamingFetchAdapter } from "./api"
import { errToStr } from "./api-helper"
import { Storage } from "./storage"

Expand Down Expand Up @@ -40,16 +41,11 @@ export class WorkspaceMonitor implements vscode.Disposable {
) {
this.name = `${workspace.owner_name}/${workspace.name}`
const url = this.restClient.getAxiosInstance().defaults.baseURL
const token = this.restClient.getAxiosInstance().defaults.headers.common["Coder-Session-Token"] as
| string
| undefined
const watchUrl = new URL(`${url}/api/v2/workspaces/${workspace.id}/watch`)
this.storage.writeToCoderOutputChannel(`Monitoring ${this.name}...`)

const eventSource = new EventSource(watchUrl.toString(), {
headers: {
"Coder-Session-Token": token,
},
fetch: createStreamingFetchAdapter(this.restClient.getAxiosInstance()),
})

eventSource.addEventListener("data", (event) => {
Expand All @@ -64,7 +60,7 @@ export class WorkspaceMonitor implements vscode.Disposable {
})

eventSource.addEventListener("error", (event) => {
this.notifyError(event.data)
this.notifyError(event)
})

// Store so we can close in dispose().
Expand Down
8 changes: 3 additions & 5 deletions src/workspacesProvider.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Api } from "coder/site/src/api/api"
import { Workspace, WorkspaceAgent } from "coder/site/src/api/typesGenerated"
import EventSource from "eventsource"
import { EventSource } from "eventsource"
import * as path from "path"
import * as vscode from "vscode"
import { createStreamingFetchAdapter } from "./api"
import {
AgentMetadataEvent,
AgentMetadataEventSchemaArray,
Expand Down Expand Up @@ -228,12 +229,9 @@ export class WorkspaceProvider implements vscode.TreeDataProvider<vscode.TreeIte
function monitorMetadata(agentId: WorkspaceAgent["id"], restClient: Api): AgentWatcher {
// TODO: Is there a better way to grab the url and token?
const url = restClient.getAxiosInstance().defaults.baseURL
const token = restClient.getAxiosInstance().defaults.headers.common["Coder-Session-Token"] as string | undefined
const metadataUrl = new URL(`${url}/api/v2/workspaceagents/${agentId}/watch-metadata`)
const eventSource = new EventSource(metadataUrl.toString(), {
headers: {
"Coder-Session-Token": token,
},
fetch: createStreamingFetchAdapter(restClient.getAxiosInstance()),
})

let disposed = false
Expand Down
25 changes: 17 additions & 8 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -537,10 +537,12 @@
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4"
integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==

"@types/eventsource@^1.1.15":
version "1.1.15"
resolved "https://registry.yarnpkg.com/@types/eventsource/-/eventsource-1.1.15.tgz#949383d3482e20557cbecbf3b038368d94b6be27"
integrity sha512-XQmGcbnxUNa06HR3VBVkc9+A2Vpi9ZyLJcdS5dwaQQ/4ZMWFO+5c90FnMUpbtMZwB/FChoYHwuVg8TvkECacTA==
"@types/eventsource@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/eventsource/-/eventsource-3.0.0.tgz#6b1b50c677032fd3be0b5c322e8ae819b3df62eb"
integrity sha512-yEhFj31FTD29DtNeqePu+A+lD6loRef6YOM5XfN1kUwBHyy2DySGlA3jJU+FbQSkrfmlBVluf2Dub/OyReFGKA==
dependencies:
eventsource "*"

"@types/glob@^7.1.3":
version "7.2.0"
Expand Down Expand Up @@ -2529,10 +2531,17 @@ events@^3.2.0:
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==

eventsource@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-2.0.2.tgz#76dfcc02930fb2ff339520b6d290da573a9e8508"
integrity sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==
eventsource-parser@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/eventsource-parser/-/eventsource-parser-3.0.0.tgz#9303e303ef807d279ee210a17ce80f16300d9f57"
integrity sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA==

eventsource@*, eventsource@^3.0.5:
version "3.0.5"
resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-3.0.5.tgz#0cae1eee2d2c75894de8b02a91d84e5c57f0cc5a"
integrity sha512-LT/5J605bx5SNyE+ITBDiM3FxffBiq9un7Vx0EwMDM3vg8sWKx/tO2zC+LMqZ+smAM0F2hblaDZUVZF0te2pSw==
dependencies:
eventsource-parser "^3.0.0"

exec@^0.2.1:
version "0.2.1"
Expand Down