Skip to content

Commit e88f289

Browse files
authored
Fix HttpRunner double-slash routing (#5946)
1 parent 245c3e6 commit e88f289

File tree

3 files changed

+117
-2
lines changed

3 files changed

+117
-2
lines changed

.changeset/deep-books-stand.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@effect/cluster": patch
3+
---
4+
5+
Fix HttpRunner double-slash routing

packages/cluster/src/HttpRunner.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import type { RunnerStorage } from "./RunnerStorage.js"
2323
import * as Sharding from "./Sharding.js"
2424
import type * as ShardingConfig from "./ShardingConfig.js"
2525

26+
const normalizePath = (path: string): string => path.startsWith("/") ? path : `/${path}`
27+
2628
/**
2729
* @since 1.0.0
2830
* @category Layers
@@ -43,7 +45,9 @@ export const layerClientProtocolHttp = (options: {
4345
return (address) => {
4446
const clientWithUrl = HttpClient.mapRequest(
4547
client,
46-
HttpClientRequest.prependUrl(`http${https ? "s" : ""}://${address.host}:${address.port}/${options.path}`)
48+
HttpClientRequest.prependUrl(
49+
`http${https ? "s" : ""}://${address.host}:${address.port}${normalizePath(options.path)}`
50+
)
4751
)
4852
return RpcClient.makeProtocolHttp(clientWithUrl).pipe(
4953
Effect.provideService(RpcSerialization.RpcSerialization, serialization)
@@ -81,7 +85,7 @@ export const layerClientProtocolWebsocket = (options: {
8185
const constructor = yield* Socket.WebSocketConstructor
8286
return Effect.fnUntraced(function*(address) {
8387
const socket = yield* Socket.makeWebSocket(
84-
`ws${https ? "s" : ""}://${address.host}:${address.port}/${options.path}`
88+
`ws${https ? "s" : ""}://${address.host}:${address.port}${normalizePath(options.path)}`
8589
).pipe(
8690
Effect.provideService(Socket.WebSocketConstructor, constructor)
8791
)
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import * as HttpClient from "@effect/platform/HttpClient"
2+
import * as HttpClientError from "@effect/platform/HttpClientError"
3+
import { RpcSerialization } from "@effect/rpc"
4+
import { describe, expect, it } from "@effect/vitest"
5+
import { Effect, Layer, Ref } from "effect"
6+
import * as HttpRunner from "../src/HttpRunner.js"
7+
import { RunnerAddress, Runners } from "../src/index.js"
8+
9+
describe("HttpRunner", () => {
10+
describe("layerClientProtocolHttp", () => {
11+
const makeUrlCapturingClient = (urlRef: Ref.Ref<Array<string>>) =>
12+
HttpClient.make((request, url) =>
13+
Ref.update(urlRef, (urls) => [...urls, url.toString()]).pipe(
14+
Effect.flatMap(() =>
15+
Effect.fail(
16+
new HttpClientError.RequestError({
17+
request,
18+
reason: "Transport",
19+
cause: new Error("Mock - URL captured")
20+
})
21+
)
22+
)
23+
)
24+
)
25+
26+
const testRequest = {
27+
_tag: "Request" as const,
28+
id: "1",
29+
tag: "test",
30+
payload: {},
31+
headers: [] as ReadonlyArray<[string, string]>
32+
}
33+
34+
it.scoped("path '/' produces http://host:port/", () =>
35+
Effect.gen(function*() {
36+
const urlRef = yield* Ref.make<Array<string>>([])
37+
38+
const layer = HttpRunner.layerClientProtocolHttp({ path: "/" }).pipe(
39+
Layer.provide(Layer.succeed(HttpClient.HttpClient, makeUrlCapturingClient(urlRef))),
40+
Layer.provide(RpcSerialization.layerNdjson)
41+
)
42+
43+
const makeProtocol = yield* Effect.provide(Runners.RpcClientProtocol, layer)
44+
const protocol = yield* makeProtocol(RunnerAddress.make("localhost", 3000))
45+
46+
yield* protocol.send(testRequest).pipe(Effect.ignore)
47+
48+
const urls = yield* Ref.get(urlRef)
49+
expect(urls[0]).toBe("http://localhost:3000/")
50+
}))
51+
52+
it.scoped("path '' produces http://host:port/", () =>
53+
Effect.gen(function*() {
54+
const urlRef = yield* Ref.make<Array<string>>([])
55+
56+
const layer = HttpRunner.layerClientProtocolHttp({ path: "" }).pipe(
57+
Layer.provide(Layer.succeed(HttpClient.HttpClient, makeUrlCapturingClient(urlRef))),
58+
Layer.provide(RpcSerialization.layerNdjson)
59+
)
60+
61+
const makeProtocol = yield* Effect.provide(Runners.RpcClientProtocol, layer)
62+
const protocol = yield* makeProtocol(RunnerAddress.make("localhost", 3000))
63+
64+
yield* protocol.send(testRequest).pipe(Effect.ignore)
65+
66+
const urls = yield* Ref.get(urlRef)
67+
expect(urls[0]).toBe("http://localhost:3000/")
68+
}))
69+
70+
it.scoped("path '/rpc' produces http://host:port/rpc", () =>
71+
Effect.gen(function*() {
72+
const urlRef = yield* Ref.make<Array<string>>([])
73+
74+
const layer = HttpRunner.layerClientProtocolHttp({ path: "/rpc" }).pipe(
75+
Layer.provide(Layer.succeed(HttpClient.HttpClient, makeUrlCapturingClient(urlRef))),
76+
Layer.provide(RpcSerialization.layerNdjson)
77+
)
78+
79+
const makeProtocol = yield* Effect.provide(Runners.RpcClientProtocol, layer)
80+
const protocol = yield* makeProtocol(RunnerAddress.make("localhost", 3000))
81+
82+
yield* protocol.send(testRequest).pipe(Effect.ignore)
83+
84+
const urls = yield* Ref.get(urlRef)
85+
expect(urls[0]).toBe("http://localhost:3000/rpc")
86+
}))
87+
88+
it.scoped("path 'rpc' produces http://host:port/rpc", () =>
89+
Effect.gen(function*() {
90+
const urlRef = yield* Ref.make<Array<string>>([])
91+
92+
const layer = HttpRunner.layerClientProtocolHttp({ path: "rpc" }).pipe(
93+
Layer.provide(Layer.succeed(HttpClient.HttpClient, makeUrlCapturingClient(urlRef))),
94+
Layer.provide(RpcSerialization.layerNdjson)
95+
)
96+
97+
const makeProtocol = yield* Effect.provide(Runners.RpcClientProtocol, layer)
98+
const protocol = yield* makeProtocol(RunnerAddress.make("localhost", 3000))
99+
100+
yield* protocol.send(testRequest).pipe(Effect.ignore)
101+
102+
const urls = yield* Ref.get(urlRef)
103+
expect(urls[0]).toBe("http://localhost:3000/rpc")
104+
}))
105+
})
106+
})

0 commit comments

Comments
 (0)