Skip to content

Commit 12a71ee

Browse files
committed
Use Axios client for EventSource
This allows client TLS certificates to be used for event monitoring. Upgrade to a newer `eventsource` package that supports a custom `fetch` function, and provide a custom `fetch` function which wraps Axios.
1 parent d6b798e commit 12a71ee

7 files changed

+95
-30
lines changed

CHANGELOG.md

+5-3
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,20 @@
22

33
## Unreleased
44

5-
## [v1.4.1](https://github.com/coder/vscode-coder/releases/tag/v1.3.9) (2025-02-19)
5+
- Use Axios client to receive event stream so TLS settings are properly applied.
66

77
### Fixed
88

9+
## [v1.4.1](https://github.com/coder/vscode-coder/releases/tag/v1.4.1) (2025-02-19)
10+
911
- Recreate REST client in spots where confirmStart may have waited indefinitely.
1012

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

1315
- Recreate REST client after starting a workspace to ensure fresh TLS certificates.
1416
- Use `coder ssh` subcommand in place of `coder vscodessh`.
1517

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

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

package.json

+6-6
Original file line numberDiff line numberDiff line change
@@ -208,10 +208,10 @@
208208
],
209209
"menus": {
210210
"commandPalette": [
211-
{
212-
"command": "coder.openFromSidebar",
213-
"when": "false"
214-
}
211+
{
212+
"command": "coder.openFromSidebar",
213+
"when": "false"
214+
}
215215
],
216216
"view/title": [
217217
{
@@ -275,7 +275,7 @@
275275
"test:ci": "CI=true yarn test"
276276
},
277277
"devDependencies": {
278-
"@types/eventsource": "^1.1.15",
278+
"@types/eventsource": "^3.0.0",
279279
"@types/glob": "^7.1.3",
280280
"@types/node": "^18.0.0",
281281
"@types/node-forge": "^1.3.11",
@@ -309,7 +309,7 @@
309309
"dependencies": {
310310
"axios": "1.7.7",
311311
"date-fns": "^3.6.0",
312-
"eventsource": "^2.0.2",
312+
"eventsource": "^3.0.5",
313313
"find-process": "^1.4.7",
314314
"jsonc-parser": "^3.3.1",
315315
"memfs": "^4.9.3",

src/api-helper.ts

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { isApiError, isApiErrorResponse } from "coder/site/src/api/errors"
22
import { Workspace, WorkspaceAgent } from "coder/site/src/api/typesGenerated"
3+
import { ErrorEvent } from "eventsource"
34
import { z } from "zod"
45

56
export function errToStr(error: unknown, def: string) {
@@ -9,6 +10,8 @@ export function errToStr(error: unknown, def: string) {
910
return error.response.data.message
1011
} else if (isApiErrorResponse(error)) {
1112
return error.message
13+
} else if (error instanceof ErrorEvent) {
14+
return error.code ? `${error.code}: ${error.message}` : error.message?.toString() || def
1215
} else if (typeof error === "string" && error.trim().length > 0) {
1316
return error
1417
}

src/api.ts

+57
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import { AxiosInstance } from "axios"
12
import { spawn } from "child_process"
23
import { Api } from "coder/site/src/api/api"
34
import { ProvisionerJobLog, Workspace } from "coder/site/src/api/typesGenerated"
5+
import { FetchLikeInit } from "eventsource"
46
import fs from "fs/promises"
57
import { ProxyAgent } from "proxy-agent"
68
import * as vscode from "vscode"
@@ -120,6 +122,60 @@ export async function makeCoderSdk(baseUrl: string, token: string | undefined, s
120122
return restClient
121123
}
122124

125+
/**
126+
* Creates a fetch adapter using an Axios instance that returns streaming responses.
127+
* This can be used with APIs that accept fetch-like interfaces.
128+
*/
129+
export function createStreamingFetchAdapter(axiosInstance: AxiosInstance) {
130+
return async (url: string | URL, init?: FetchLikeInit) => {
131+
const urlStr = url.toString()
132+
133+
const response = await axiosInstance.request({
134+
url: urlStr,
135+
headers: init?.headers as Record<string, string>,
136+
responseType: "stream",
137+
validateStatus: () => true, // Don't throw on any status code
138+
})
139+
const stream = new ReadableStream({
140+
start(controller) {
141+
response.data.on("data", (chunk: Buffer) => {
142+
controller.enqueue(chunk)
143+
})
144+
145+
response.data.on("end", () => {
146+
controller.close()
147+
})
148+
149+
response.data.on("error", (err: Error) => {
150+
controller.error(err)
151+
})
152+
},
153+
154+
cancel() {
155+
response.data.destroy()
156+
return Promise.resolve()
157+
},
158+
})
159+
160+
const createReader = () => stream.getReader()
161+
162+
return {
163+
body: {
164+
getReader: () => createReader(),
165+
},
166+
url: urlStr,
167+
status: response.status,
168+
redirected: response.request.res.responseUrl !== urlStr,
169+
headers: {
170+
get: (name: string) => {
171+
const value = response.headers[name.toLowerCase()]
172+
return value === undefined ? null : String(value)
173+
},
174+
},
175+
}
176+
}
177+
}
178+
123179
/**
124180
* Start or update a workspace and return the updated workspace.
125181
*/
@@ -224,6 +280,7 @@ export async function waitForBuild(
224280
| undefined,
225281
},
226282
followRedirects: true,
283+
agent: restClient.getAxiosInstance().defaults.httpAgent,
227284
})
228285
socket.binaryType = "nodebuffer"
229286
socket.on("message", (data) => {

src/workspaceMonitor.ts

+4-8
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { Api } from "coder/site/src/api/api"
22
import { Workspace } from "coder/site/src/api/typesGenerated"
33
import { formatDistanceToNowStrict } from "date-fns"
4-
import EventSource from "eventsource"
4+
import { EventSource } from "eventsource"
55
import * as vscode from "vscode"
6+
import { createStreamingFetchAdapter } from "./api"
67
import { errToStr } from "./api-helper"
78
import { Storage } from "./storage"
89

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

4947
const eventSource = new EventSource(watchUrl.toString(), {
50-
headers: {
51-
"Coder-Session-Token": token,
52-
},
48+
fetch: createStreamingFetchAdapter(this.restClient.getAxiosInstance()),
5349
})
5450

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

6662
eventSource.addEventListener("error", (event) => {
67-
this.notifyError(event.data)
63+
this.notifyError(event)
6864
})
6965

7066
// Store so we can close in dispose().

src/workspacesProvider.ts

+3-5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { Api } from "coder/site/src/api/api"
22
import { Workspace, WorkspaceAgent } from "coder/site/src/api/typesGenerated"
3-
import EventSource from "eventsource"
3+
import { EventSource } from "eventsource"
44
import * as path from "path"
55
import * as vscode from "vscode"
6+
import { createStreamingFetchAdapter } from "./api"
67
import {
78
AgentMetadataEvent,
89
AgentMetadataEventSchemaArray,
@@ -228,12 +229,9 @@ export class WorkspaceProvider implements vscode.TreeDataProvider<vscode.TreeIte
228229
function monitorMetadata(agentId: WorkspaceAgent["id"], restClient: Api): AgentWatcher {
229230
// TODO: Is there a better way to grab the url and token?
230231
const url = restClient.getAxiosInstance().defaults.baseURL
231-
const token = restClient.getAxiosInstance().defaults.headers.common["Coder-Session-Token"] as string | undefined
232232
const metadataUrl = new URL(`${url}/api/v2/workspaceagents/${agentId}/watch-metadata`)
233233
const eventSource = new EventSource(metadataUrl.toString(), {
234-
headers: {
235-
"Coder-Session-Token": token,
236-
},
234+
fetch: createStreamingFetchAdapter(restClient.getAxiosInstance()),
237235
})
238236

239237
let disposed = false

yarn.lock

+17-8
Original file line numberDiff line numberDiff line change
@@ -537,10 +537,12 @@
537537
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4"
538538
integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==
539539

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

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

2532-
eventsource@^2.0.2:
2533-
version "2.0.2"
2534-
resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-2.0.2.tgz#76dfcc02930fb2ff339520b6d290da573a9e8508"
2535-
integrity sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==
2534+
eventsource-parser@^3.0.0:
2535+
version "3.0.0"
2536+
resolved "https://registry.yarnpkg.com/eventsource-parser/-/eventsource-parser-3.0.0.tgz#9303e303ef807d279ee210a17ce80f16300d9f57"
2537+
integrity sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA==
2538+
2539+
eventsource@*, eventsource@^3.0.5:
2540+
version "3.0.5"
2541+
resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-3.0.5.tgz#0cae1eee2d2c75894de8b02a91d84e5c57f0cc5a"
2542+
integrity sha512-LT/5J605bx5SNyE+ITBDiM3FxffBiq9un7Vx0EwMDM3vg8sWKx/tO2zC+LMqZ+smAM0F2hblaDZUVZF0te2pSw==
2543+
dependencies:
2544+
eventsource-parser "^3.0.0"
25362545

25372546
exec@^0.2.1:
25382547
version "0.2.1"

0 commit comments

Comments
 (0)