Skip to content

Commit fecbc71

Browse files
{  "message": "Bad credentials",  "documentation_url": "https://docs.github.com/rest",  "status": "401"}{  "message": "Bad credentials",  "documentation_url": "https://docs.github.com/rest",  "status": "401"}
authored andcommitted
ci: avoid bun fetch timeout in host cli
1 parent 5de63b8 commit fecbc71

2 files changed

Lines changed: 75 additions & 55 deletions

File tree

packages/app/src/docker-git/api-http.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { HttpClientResponse } from "@effect/platform"
2-
import { FetchHttpClient, HttpBody, HttpClient } from "@effect/platform"
2+
import { HttpBody, HttpClient } from "@effect/platform"
33
import type * as HttpClientError from "@effect/platform/HttpClientError"
4+
import { NodeHttpClient } from "@effect/platform-node"
45
import { Effect } from "effect"
56

67
import { readHttpResponseTextStream } from "../shared/http-response-stream.js"
@@ -201,7 +202,7 @@ export const request = (
201202
}
202203

203204
return parsed
204-
}).pipe(Effect.provide(FetchHttpClient.layer), mapTransportError(method, path))
205+
}).pipe(Effect.provide(NodeHttpClient.layer), mapTransportError(method, path))
205206

206207
export const requestVoid = (method: ApiHttpMethod, path: string, body?: JsonRequest) =>
207208
request(method, path, body).pipe(Effect.asVoid)
@@ -222,4 +223,4 @@ export const requestTextStream = (
222223
}
223224

224225
return yield* _(readHttpResponseTextStream(response, onChunk))
225-
}).pipe(Effect.provide(FetchHttpClient.layer), mapTransportError(method, path))
226+
}).pipe(Effect.provide(NodeHttpClient.layer), mapTransportError(method, path))
Lines changed: 71 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1+
/* jscpd:ignore-start */
2+
import { createServer, type Server } from "node:http"
3+
import type { AddressInfo } from "node:net"
4+
15
import { NodeContext } from "@effect/platform-node"
26
import { describe, expect, it } from "@effect/vitest"
37
import { Effect } from "effect"
4-
import { afterEach, beforeEach, vi } from "vitest"
8+
import { beforeEach, vi } from "vitest"
59

610
import { request } from "../../src/docker-git/api-http.js"
11+
/* jscpd:ignore-end */
712

813
const resolveApiBaseUrlMock = vi.hoisted(() => vi.fn<() => string>())
914
const ensureControllerReadyMock = vi.hoisted(() => vi.fn<() => Effect.Effect<void>>())
@@ -15,22 +20,42 @@ vi.mock("../../src/docker-git/controller.js", () => ({
1520

1621
const joinIp = (...octets: ReadonlyArray<string>): string => octets.join(".")
1722
const makeHttpUrl = (host: string, port: string): string => ["ht", "tp://", host, ":", port].join("")
18-
const toFetchUrl = (value: Parameters<typeof globalThis.fetch>[0] | undefined): string => {
19-
if (value === undefined) {
20-
throw new TypeError("unexpected undefined fetch request value")
21-
}
22-
if (typeof value === "string") {
23-
return value
24-
}
25-
if (value instanceof URL) {
26-
return value.toString()
27-
}
28-
if (value instanceof Request) {
29-
return value.url
30-
}
31-
32-
throw new TypeError("unexpected fetch request value")
33-
}
23+
24+
const listen = (server: Server): Effect.Effect<number, Error> =>
25+
Effect.async((resume) => {
26+
const onError = (error: Error) => {
27+
resume(Effect.fail(error))
28+
}
29+
30+
server.once("error", onError)
31+
server.listen(0, "127.0.0.1", () => {
32+
server.off("error", onError)
33+
resume(Effect.succeed((server.address() as AddressInfo).port))
34+
})
35+
36+
return Effect.sync(() => {
37+
server.off("error", onError)
38+
})
39+
})
40+
41+
const close = (server: Server): Effect.Effect<void, Error> =>
42+
Effect.async((resume) => {
43+
server.close((error) => {
44+
if (error === undefined) {
45+
resume(Effect.void)
46+
return
47+
}
48+
resume(Effect.fail(error))
49+
})
50+
})
51+
52+
const reserveUnusedPort = () =>
53+
Effect.gen(function*(_) {
54+
const server = createServer()
55+
const port = yield* _(listen(server))
56+
yield* _(close(server))
57+
return port
58+
})
3459

3560
describe("api-http request retry", () => {
3661
beforeEach(() => {
@@ -39,55 +64,49 @@ describe("api-http request retry", () => {
3964
ensureControllerReadyMock.mockImplementation(() => Effect.void)
4065
})
4166

42-
afterEach(() => {
43-
vi.unstubAllGlobals()
44-
})
45-
4667
it.effect("refreshes controller readiness once after a transport failure", () =>
4768
Effect.gen(function*(_) {
48-
const fetchMock = vi.fn<typeof globalThis.fetch>()
49-
fetchMock.mockRejectedValueOnce(new TypeError("fetch failed"))
50-
fetchMock.mockResolvedValueOnce(
51-
Response.json({ ok: true }, {
52-
status: 200,
53-
headers: { "content-type": "application/json" }
54-
})
55-
)
56-
vi.stubGlobal("fetch", fetchMock)
57-
58-
resolveApiBaseUrlMock.mockReturnValueOnce(
59-
makeHttpUrl(joinIp("127", "0", "0", "1"), "3334")
60-
)
61-
resolveApiBaseUrlMock.mockReturnValueOnce(
62-
makeHttpUrl(joinIp("172", "17", "0", "20"), "3334")
69+
const seenUrls: Array<string | undefined> = []
70+
const server = createServer((incoming, response) => {
71+
seenUrls.push(incoming.url)
72+
response.writeHead(200, { "content-type": "application/json" })
73+
response.end(JSON.stringify({ ok: true }))
74+
})
75+
const deadPort = yield* _(reserveUnusedPort())
76+
const port = yield* _(listen(server))
77+
78+
yield* _(
79+
Effect.gen(function*(_) {
80+
resolveApiBaseUrlMock.mockReturnValueOnce(
81+
makeHttpUrl(joinIp("127", "0", "0", "1"), String(deadPort))
82+
)
83+
resolveApiBaseUrlMock.mockReturnValueOnce(
84+
makeHttpUrl(joinIp("127", "0", "0", "1"), String(port))
85+
)
86+
87+
const payload = yield* _(request("GET", "/health"))
88+
89+
expect(payload).toEqual({ ok: true })
90+
expect(ensureControllerReadyMock).toHaveBeenCalledTimes(1)
91+
expect(seenUrls).toEqual(["/health"])
92+
}).pipe(
93+
Effect.ensuring(close(server).pipe(Effect.catchAll(() => Effect.void)))
94+
)
6395
)
64-
65-
const payload = yield* _(request("GET", "/health"))
66-
67-
expect(payload).toEqual({ ok: true })
68-
expect(ensureControllerReadyMock).toHaveBeenCalledTimes(1)
69-
expect(fetchMock).toHaveBeenCalledTimes(2)
70-
71-
const firstCall = fetchMock.mock.calls[0]?.[0]
72-
const secondCall = fetchMock.mock.calls[1]?.[0]
73-
expect(toFetchUrl(firstCall)).toContain(`${joinIp("127", "0", "0", "1")}:3334/health`)
74-
expect(toFetchUrl(secondCall)).toContain(`${joinIp("172", "17", "0", "20")}:3334/health`)
7596
}).pipe(Effect.provide(NodeContext.layer)))
7697

7798
it.effect("does not replay mutating requests after a transport failure", () =>
7899
Effect.gen(function*(_) {
79-
const fetchMock = vi.fn<typeof globalThis.fetch>()
80-
fetchMock.mockRejectedValueOnce(new TypeError("fetch failed"))
81-
vi.stubGlobal("fetch", fetchMock)
100+
const deadPort = yield* _(reserveUnusedPort())
82101

83102
resolveApiBaseUrlMock.mockReturnValue(
84-
makeHttpUrl(joinIp("127", "0", "0", "1"), "3334")
103+
makeHttpUrl(joinIp("127", "0", "0", "1"), String(deadPort))
85104
)
86105

87106
const result = yield* _(Effect.either(request("POST", "/projects", { outDir: "project-1" })))
88107

89108
expect(result._tag).toBe("Left")
90109
expect(ensureControllerReadyMock).not.toHaveBeenCalled()
91-
expect(fetchMock).toHaveBeenCalledTimes(1)
110+
expect(resolveApiBaseUrlMock).toHaveBeenCalledTimes(1)
92111
}).pipe(Effect.provide(NodeContext.layer)))
93112
})

0 commit comments

Comments
 (0)