Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ A breaking change will get clearly marked in this log.

## Unreleased

### Fixed
* `CallBuilder` now correctly uses the configured server URL for all requests, including pagination and linked resources. Previously, URLs returned by Horizon in `_links` would bypass reverse proxies ([#1318](https://github.com/stellar/js-stellar-sdk/pull/1318)).

## [v14.4.3](https://github.com/stellar/js-stellar-sdk/compare/v14.4.2...v14.4.3)

Expand Down
10 changes: 3 additions & 7 deletions src/horizon/call_builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,13 +377,9 @@ export class CallBuilder<
private async _sendNormalRequest(initialUrl: URI) {
let url = initialUrl;

if (url.authority() === "") {
url = url.authority(this.url.authority());
}

if (url.protocol() === "") {
url = url.protocol(this.url.protocol());
}
// Always use the configured server's authority and protocol.
// Horizon returns absolute URLs in _links that would bypass reverse proxies.
url = url.authority(this.url.authority()).protocol(this.url.protocol());

return this.httpClient
.get(url.toString())
Expand Down
85 changes: 85 additions & 0 deletions test/integration/client_headers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,4 +126,89 @@ describe("integration tests: client headers", () => {
.operations()
.call();
});

it("uses configured server URL for pagination links (reverse proxy support)", async () => {
let server: http.Server;
let requestCount = 0;

const requestHandler = (
_request: http.IncomingMessage,
response: http.ServerResponse,
) => {
requestCount++;

if (requestCount === 1) {
// First request: return a response with _links pointing to a DIFFERENT host
// This simulates what Horizon does - it returns its own hostname in links
response.setHeader("Content-Type", "application/json");
response.end(
JSON.stringify({
_embedded: {
records: [{ id: "1", paging_token: "token1" }],
},
_links: {
// These links point to a different host (horizon.stellar.org)
// The SDK should rewrite these to use localhost:${port}
next: {
href: `https://horizon.stellar.org/operations?cursor=token1`,
},
prev: {
href: `https://horizon.stellar.org/operations?cursor=token0`,
},
},
}),
);
} else if (requestCount === 2) {
// Second request (pagination): verify it came to our server, not horizon.stellar.org
response.setHeader("Content-Type", "application/json");
response.end(
JSON.stringify({
_embedded: {
records: [{ id: "2", paging_token: "token2" }],
},
_links: {
next: {
href: `https://horizon.stellar.org/operations?cursor=token2`,
},
prev: {
href: `https://horizon.stellar.org/operations?cursor=token1`,
},
},
}),
);
server.close();
}
};

server = http.createServer(requestHandler);

await new Promise<void>((resolve, reject) => {
server.listen(port, (err?: Error) => {
if (err) {
reject(err);
return;
}
resolve();
});
});

const horizonServer = new Horizon.Server(`http://localhost:${port}`, {
allowHttp: true,
});

// First request
const firstPage = await horizonServer.operations().call();
expect(firstPage.records).toHaveLength(1);
expect(firstPage.records[0]!.id).toBe("1");

// Second request via .next() - this should go to localhost, not horizon.stellar.org
// If the fix works, requestCount will be 2. If not, this will timeout/fail
// because the request would go to horizon.stellar.org instead of our mock server
const secondPage = await firstPage.next();
expect(secondPage.records).toHaveLength(1);
expect(secondPage.records[0]!.id).toBe("2");

// Verify both requests came to our server
expect(requestCount).toBe(2);
});
});
5 changes: 3 additions & 2 deletions test/unit/server/horizon/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -802,7 +802,7 @@
}
if (
url.match(
/^https:\/\/horizon.stellar.org\/transactions\/c585b8764b28be678c482f8b6e87e76e4b5f28043c53f4dcb7b724b4b2efebc1\/operations/,
/^https:\/\/horizon-live.stellar.org:1337\/transactions\/c585b8764b28be678c482f8b6e87e76e4b5f28043c53f4dcb7b724b4b2efebc1\/operations/,
Comment thread Fixed
)
) {
return Promise.resolve({ data: { operations: [] } });
Expand Down Expand Up @@ -830,6 +830,7 @@
describe("with options", () => {
it("requests the correct endpoint", async () => {
mockGet.mockImplementation((url: string) => {
console.log("URL called:", url);
if (
url.includes(
"https://horizon-live.stellar.org:1337/ledgers/7952722/transactions?cursor=b&limit=1&order=asc",
Expand All @@ -839,7 +840,7 @@
}
if (
url.match(
/^https:\/\/horizon.stellar.org\/transactions\/c585b8764b28be678c482f8b6e87e76e4b5f28043c53f4dcb7b724b4b2efebc1\/operations\?limit=1/,
/^https:\/\/horizon-live.stellar.org:1337\/transactions\/c585b8764b28be678c482f8b6e87e76e4b5f28043c53f4dcb7b724b4b2efebc1\/operations\?limit=1/,
Comment thread Fixed
)
) {
return Promise.resolve({ data: { operations: [] } });
Expand Down
Loading