Skip to content
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
19 changes: 19 additions & 0 deletions .changeset/fix-append-url-path-joining.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
"@effect/platform": patch
---

Fix `HttpClientRequest.appendUrl` to properly join URL paths.

Previously, `appendUrl` used simple string concatenation which could produce invalid URLs:
```typescript
// Before (broken):
appendUrl("https://api.example.com/v1", "users")
// Result: "https://api.example.com/v1users" (missing slash!)
```

Now it ensures proper path joining:
```typescript
// After (fixed):
appendUrl("https://api.example.com/v1", "users")
// Result: "https://api.example.com/v1/users"
```
16 changes: 10 additions & 6 deletions packages/platform/src/internal/httpClientRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,17 +250,21 @@ export const setUrl = dual<
export const appendUrl = dual<
(path: string) => (self: ClientRequest.HttpClientRequest) => ClientRequest.HttpClientRequest,
(self: ClientRequest.HttpClientRequest, path: string) => ClientRequest.HttpClientRequest
>(2, (self, url) =>
makeInternal(
>(2, (self, path) => {
if (path === "") {
return self
}
const baseUrl = self.url.endsWith("/") ? self.url : self.url + "/"
const pathSegment = path.startsWith("/") ? path.slice(1) : path
return makeInternal(
self.method,
self.url.endsWith("/") && url.startsWith("/") ?
self.url + url.slice(1) :
self.url + url,
baseUrl + pathSegment,
self.urlParams,
self.hash,
self.headers,
self.body
))
)
})

/** @internal */
export const prependUrl = dual<
Expand Down
44 changes: 44 additions & 0 deletions packages/platform/test/HttpClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,50 @@ describe("HttpClient", () => {
)
})

describe("appendUrl", () => {
it("joins path without trailing slash on base", () => {
const request = HttpClientRequest.get("https://api.example.com/v1").pipe(
HttpClientRequest.appendUrl("users")
)
strictEqual(request.url, "https://api.example.com/v1/users")
})

it("joins path with trailing slash on base", () => {
const request = HttpClientRequest.get("https://api.example.com/v1/").pipe(
HttpClientRequest.appendUrl("users")
)
strictEqual(request.url, "https://api.example.com/v1/users")
})

it("joins path with leading slash", () => {
const request = HttpClientRequest.get("https://api.example.com/v1").pipe(
HttpClientRequest.appendUrl("/users")
)
strictEqual(request.url, "https://api.example.com/v1/users")
})

it("joins path with both trailing and leading slashes", () => {
const request = HttpClientRequest.get("https://api.example.com/v1/").pipe(
HttpClientRequest.appendUrl("/users")
)
strictEqual(request.url, "https://api.example.com/v1/users")
})

it("joins nested paths", () => {
const request = HttpClientRequest.get("https://api.example.com/v1").pipe(
HttpClientRequest.appendUrl("users/123/posts")
)
strictEqual(request.url, "https://api.example.com/v1/users/123/posts")
})

it("handles empty path", () => {
const request = HttpClientRequest.get("https://api.example.com/v1").pipe(
HttpClientRequest.appendUrl("")
)
strictEqual(request.url, "https://api.example.com/v1")
})
})

it.effect("matchStatus", () =>
Effect.gen(function*() {
const jp = yield* JsonPlaceholder
Expand Down