Skip to content

Commit 0023c19

Browse files
fix(platform): properly join URL paths in HttpClientRequest.appendUrl (#6021)
1 parent 7b8165f commit 0023c19

File tree

3 files changed

+73
-6
lines changed

3 files changed

+73
-6
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
"@effect/platform": patch
3+
---
4+
5+
Fix `HttpClientRequest.appendUrl` to properly join URL paths.
6+
7+
Previously, `appendUrl` used simple string concatenation which could produce invalid URLs:
8+
```typescript
9+
// Before (broken):
10+
appendUrl("https://api.example.com/v1", "users")
11+
// Result: "https://api.example.com/v1users" (missing slash!)
12+
```
13+
14+
Now it ensures proper path joining:
15+
```typescript
16+
// After (fixed):
17+
appendUrl("https://api.example.com/v1", "users")
18+
// Result: "https://api.example.com/v1/users"
19+
```

packages/platform/src/internal/httpClientRequest.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -250,17 +250,21 @@ export const setUrl = dual<
250250
export const appendUrl = dual<
251251
(path: string) => (self: ClientRequest.HttpClientRequest) => ClientRequest.HttpClientRequest,
252252
(self: ClientRequest.HttpClientRequest, path: string) => ClientRequest.HttpClientRequest
253-
>(2, (self, url) =>
254-
makeInternal(
253+
>(2, (self, path) => {
254+
if (path === "") {
255+
return self
256+
}
257+
const baseUrl = self.url.endsWith("/") ? self.url : self.url + "/"
258+
const pathSegment = path.startsWith("/") ? path.slice(1) : path
259+
return makeInternal(
255260
self.method,
256-
self.url.endsWith("/") && url.startsWith("/") ?
257-
self.url + url.slice(1) :
258-
self.url + url,
261+
baseUrl + pathSegment,
259262
self.urlParams,
260263
self.hash,
261264
self.headers,
262265
self.body
263-
))
266+
)
267+
})
264268

265269
/** @internal */
266270
export const prependUrl = dual<

packages/platform/test/HttpClient.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,50 @@ describe("HttpClient", () => {
202202
)
203203
})
204204

205+
describe("appendUrl", () => {
206+
it("joins path without trailing slash on base", () => {
207+
const request = HttpClientRequest.get("https://api.example.com/v1").pipe(
208+
HttpClientRequest.appendUrl("users")
209+
)
210+
strictEqual(request.url, "https://api.example.com/v1/users")
211+
})
212+
213+
it("joins path with trailing slash on base", () => {
214+
const request = HttpClientRequest.get("https://api.example.com/v1/").pipe(
215+
HttpClientRequest.appendUrl("users")
216+
)
217+
strictEqual(request.url, "https://api.example.com/v1/users")
218+
})
219+
220+
it("joins path with leading slash", () => {
221+
const request = HttpClientRequest.get("https://api.example.com/v1").pipe(
222+
HttpClientRequest.appendUrl("/users")
223+
)
224+
strictEqual(request.url, "https://api.example.com/v1/users")
225+
})
226+
227+
it("joins path with both trailing and leading slashes", () => {
228+
const request = HttpClientRequest.get("https://api.example.com/v1/").pipe(
229+
HttpClientRequest.appendUrl("/users")
230+
)
231+
strictEqual(request.url, "https://api.example.com/v1/users")
232+
})
233+
234+
it("joins nested paths", () => {
235+
const request = HttpClientRequest.get("https://api.example.com/v1").pipe(
236+
HttpClientRequest.appendUrl("users/123/posts")
237+
)
238+
strictEqual(request.url, "https://api.example.com/v1/users/123/posts")
239+
})
240+
241+
it("handles empty path", () => {
242+
const request = HttpClientRequest.get("https://api.example.com/v1").pipe(
243+
HttpClientRequest.appendUrl("")
244+
)
245+
strictEqual(request.url, "https://api.example.com/v1")
246+
})
247+
})
248+
205249
it.effect("matchStatus", () =>
206250
Effect.gen(function*() {
207251
const jp = yield* JsonPlaceholder

0 commit comments

Comments
 (0)