Skip to content

Commit 697544c

Browse files
authored
Reverse Proxy Support (#1318)
* use configured server URL for all CallBuilder requests
1 parent cafa3e6 commit 697544c

File tree

4 files changed

+93
-11
lines changed

4 files changed

+93
-11
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ A breaking change will get clearly marked in this log.
1212

1313
### Fixed
1414
* X-App-Name and X-App-Version headers are now included when using `CallBuilder.stream()` ([#1317](https://github.com/stellar/js-stellar-sdk/pull/1317)).
15-
15+
* `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)).
1616

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

src/horizon/call_builder.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -398,13 +398,9 @@ export class CallBuilder<
398398
private async _sendNormalRequest(initialUrl: URI) {
399399
let url = initialUrl;
400400

401-
if (url.authority() === "") {
402-
url = url.authority(this.url.authority());
403-
}
404-
405-
if (url.protocol() === "") {
406-
url = url.protocol(this.url.protocol());
407-
}
401+
// Always use the configured server's authority and protocol.
402+
// Horizon returns absolute URLs in _links that would bypass reverse proxies.
403+
url = url.authority(this.url.authority()).protocol(this.url.protocol());
408404

409405
return this.httpClient
410406
.get(url.toString())

test/integration/client_headers.test.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,91 @@ describe("integration tests: client headers", () => {
127127
.call();
128128
});
129129

130+
it("uses configured server URL for pagination links (reverse proxy support)", async () => {
131+
let server: http.Server;
132+
let requestCount = 0;
133+
134+
const requestHandler = (
135+
_request: http.IncomingMessage,
136+
response: http.ServerResponse,
137+
) => {
138+
requestCount++;
139+
140+
if (requestCount === 1) {
141+
// First request: return a response with _links pointing to a DIFFERENT host
142+
// This simulates what Horizon does - it returns its own hostname in links
143+
response.setHeader("Content-Type", "application/json");
144+
response.end(
145+
JSON.stringify({
146+
_embedded: {
147+
records: [{ id: "1", paging_token: "token1" }],
148+
},
149+
_links: {
150+
// These links point to a different host (horizon.stellar.org)
151+
// The SDK should rewrite these to use localhost:${port}
152+
next: {
153+
href: `https://horizon.stellar.org/operations?cursor=token1`,
154+
},
155+
prev: {
156+
href: `https://horizon.stellar.org/operations?cursor=token0`,
157+
},
158+
},
159+
}),
160+
);
161+
} else if (requestCount === 2) {
162+
// Second request (pagination): verify it came to our server, not horizon.stellar.org
163+
response.setHeader("Content-Type", "application/json");
164+
response.end(
165+
JSON.stringify({
166+
_embedded: {
167+
records: [{ id: "2", paging_token: "token2" }],
168+
},
169+
_links: {
170+
next: {
171+
href: `https://horizon.stellar.org/operations?cursor=token2`,
172+
},
173+
prev: {
174+
href: `https://horizon.stellar.org/operations?cursor=token1`,
175+
},
176+
},
177+
}),
178+
);
179+
server.close();
180+
}
181+
};
182+
183+
server = http.createServer(requestHandler);
184+
185+
await new Promise<void>((resolve, reject) => {
186+
server.listen(port, (err?: Error) => {
187+
if (err) {
188+
reject(err);
189+
return;
190+
}
191+
resolve();
192+
});
193+
});
194+
195+
const horizonServer = new Horizon.Server(`http://localhost:${port}`, {
196+
allowHttp: true,
197+
});
198+
199+
// First request
200+
const firstPage = await horizonServer.operations().call();
201+
expect(firstPage.records).toHaveLength(1);
202+
expect(firstPage.records[0]!.id).toBe("1");
203+
204+
// Second request via .next() - this should go to localhost, not horizon.stellar.org
205+
// If the fix works, requestCount will be 2. If not, this will timeout/fail
206+
// because the request would go to horizon.stellar.org instead of our mock server
207+
const secondPage = await firstPage.next();
208+
expect(secondPage.records).toHaveLength(1);
209+
expect(secondPage.records[0]!.id).toBe("2");
210+
211+
// Verify both requests came to our server
212+
expect(requestCount).toBe(2);
213+
});
214+
130215
it("sends appName and appVersion via headers for HTTP requests", async () => {
131216
let server: http.Server;
132217

test/unit/server/horizon/server.test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -802,7 +802,7 @@ describe("server.js non-transaction tests", () => {
802802
}
803803
if (
804804
url.match(
805-
/^https:\/\/horizon.stellar.org\/transactions\/c585b8764b28be678c482f8b6e87e76e4b5f28043c53f4dcb7b724b4b2efebc1\/operations/,
805+
/^https:\/\/horizon-live\.stellar\.org:1337\/transactions\/c585b8764b28be678c482f8b6e87e76e4b5f28043c53f4dcb7b724b4b2efebc1\/operations/,
806806
)
807807
) {
808808
return Promise.resolve({ data: { operations: [] } });
@@ -830,6 +830,7 @@ describe("server.js non-transaction tests", () => {
830830
describe("with options", () => {
831831
it("requests the correct endpoint", async () => {
832832
mockGet.mockImplementation((url: string) => {
833+
console.log("URL called:", url);
833834
if (
834835
url.includes(
835836
"https://horizon-live.stellar.org:1337/ledgers/7952722/transactions?cursor=b&limit=1&order=asc",
@@ -839,7 +840,7 @@ describe("server.js non-transaction tests", () => {
839840
}
840841
if (
841842
url.match(
842-
/^https:\/\/horizon.stellar.org\/transactions\/c585b8764b28be678c482f8b6e87e76e4b5f28043c53f4dcb7b724b4b2efebc1\/operations\?limit=1/,
843+
/^https:\/\/horizon-live\.stellar\.org:1337\/transactions\/c585b8764b28be678c482f8b6e87e76e4b5f28043c53f4dcb7b724b4b2efebc1\/operations\?limit=1/,
843844
)
844845
) {
845846
return Promise.resolve({ data: { operations: [] } });
@@ -940,7 +941,7 @@ describe("server.js non-transaction tests", () => {
940941
mockGet.mockImplementation((url: string) => {
941942
if (
942943
url.includes(
943-
"https://horizon-live.stellar.org:1337/transactions/6bbd8cbd90498a26210a21ec599702bead8f908f412455da300318aba36831b0",
944+
"https://horizon-live.stellar\.org:1337/transactions/6bbd8cbd90498a26210a21ec599702bead8f908f412455da300318aba36831b0",
944945
)
945946
) {
946947
return Promise.resolve({ data: singleTranssactionResponse });

0 commit comments

Comments
 (0)