Skip to content

Commit 597adef

Browse files
authored
[Recorder] Ensure redirected requests are passed to the recorder in Node (Azure#21355)
1 parent 1c9a47b commit 597adef

File tree

4 files changed

+109
-20
lines changed

4 files changed

+109
-20
lines changed

sdk/test-utils/recorder/src/recorder.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -79,21 +79,31 @@ export class Recorder {
7979
* - PipelineRequest -> core-v2 (recorderHttpPolicy calls this method on the request to modify and hit the proxy-tool with appropriate headers.)
8080
*/
8181
private redirectRequest(request: WebResource | PipelineRequest): void {
82-
if (!isLiveMode() && !request.headers.get("x-recording-id")) {
82+
const upstreamUrl = new URL(request.url);
83+
const redirectedUrl = new URL(request.url);
84+
const testProxyUrl = new URL(this.url);
85+
86+
// Sometimes, due to the service returning a redirect or due to the retry policy, redirectRequest
87+
// may be called multiple times. We only want to update the request the second time if the request's
88+
// URL has been changed between calls (this may happen in the case of a redirect, but generally
89+
// not in the case of a retry). Otherwise, we might accidentally update the X-Recording-Upstream-Base-Uri
90+
// header to point to the test proxy instead of the true upstream.
91+
const requestAlreadyRedirected =
92+
upstreamUrl.host === testProxyUrl.host &&
93+
upstreamUrl.port === testProxyUrl.port &&
94+
upstreamUrl.protocol === testProxyUrl.protocol;
95+
96+
if (!isLiveMode() && !requestAlreadyRedirected) {
8397
if (this.recordingId === undefined) {
8498
throw new RecorderError("Recording ID must be defined to redirect a request");
8599
}
86100

87101
request.headers.set("x-recording-id", this.recordingId);
88102
request.headers.set("x-recording-mode", getTestMode());
89103

90-
const upstreamUrl = new URL(request.url);
91-
const redirectedUrl = new URL(request.url);
92-
const providedUrl = new URL(this.url);
93-
94-
redirectedUrl.host = providedUrl.host;
95-
redirectedUrl.port = providedUrl.port;
96-
redirectedUrl.protocol = providedUrl.protocol;
104+
redirectedUrl.host = testProxyUrl.host;
105+
redirectedUrl.port = testProxyUrl.port;
106+
redirectedUrl.protocol = testProxyUrl.protocol;
97107
request.headers.set("x-recording-upstream-base-uri", upstreamUrl.toString());
98108
request.url = redirectedUrl.toString();
99109

sdk/test-utils/recorder/test/testProxyClient.spec.ts

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -57,17 +57,23 @@ describe("TestProxyClient functions", () => {
5757
});
5858

5959
["record", "playback"].forEach((testMode) => {
60-
it(`${testMode} mode: ` + "request unchanged if `x-recording-id` in headers", function () {
61-
env.TEST_MODE = testMode;
62-
testRedirectedRequest(
63-
client,
64-
() => ({
65-
...initialRequest,
66-
headers: createHttpHeaders({ "x-recording-id": "dummy-recording-id" }),
67-
}),
68-
(req) => req
69-
);
70-
});
60+
it(
61+
`${testMode} mode: ` + "request unchanged if request URL already points to test proxy",
62+
function () {
63+
env.TEST_MODE = testMode;
64+
testRedirectedRequest(
65+
client,
66+
() => ({
67+
...initialRequest,
68+
url: "http://localhost:5000/dummy_path?sas=sas",
69+
headers: createHttpHeaders({
70+
"x-recording-upstream-uri": "https://dummy_url.windows.net/dummy_path?sas=sas",
71+
}),
72+
}),
73+
(req) => req
74+
);
75+
}
76+
);
7177

7278
it(
7379
`${testMode} mode: ` + "url and headers get updated if no `x-recording-id` in headers",

sdk/test-utils/recorder/test/testProxyTests.spec.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT license.
33

44
import { ServiceClient } from "@azure/core-client";
5+
import { isNode } from "@azure/core-util";
56
import { CustomMatcherOptions, isPlaybackMode, Recorder } from "../src";
67
import { isLiveMode, TestMode } from "../src/utils/utils";
78
import { getTestServerUrl, makeRequestAndVerifyResponse, setTestMode } from "./utils/utils";
@@ -36,6 +37,46 @@ import { getTestServerUrl, makeRequestAndVerifyResponse, setTestMode } from "./u
3637
);
3738
});
3839

40+
it("redirect (redirect location has host)", async function (this: Mocha.Context) {
41+
await recorder.start({ envSetupForPlayback: {} });
42+
43+
if (!isNode) {
44+
// In the browser, redirects get handled by fetch/XHR and we can't guarantee redirect behavior.
45+
this.skip();
46+
}
47+
48+
await makeRequestAndVerifyResponse(
49+
client,
50+
{ path: `/redirectWithHost`, method: "GET" },
51+
{ val: "abc" }
52+
);
53+
});
54+
55+
it("redirect (redirect location is relative)", async function (this: Mocha.Context) {
56+
await recorder.start({ envSetupForPlayback: {} });
57+
58+
if (!isNode) {
59+
// In the browser, redirects get handled by fetch/XHR and we can't guarantee redirect behavior.
60+
this.skip();
61+
}
62+
63+
await makeRequestAndVerifyResponse(
64+
client,
65+
{ path: `/redirectWithoutHost`, method: "GET" },
66+
{ val: "abc" }
67+
);
68+
});
69+
70+
it("retry", async () => {
71+
await recorder.start({ envSetupForPlayback: {} });
72+
await makeRequestAndVerifyResponse(
73+
client,
74+
{ path: "/reset_retry", method: "GET" },
75+
undefined
76+
);
77+
await makeRequestAndVerifyResponse(client, { path: "/retry", method: "GET" }, { val: "abc" });
78+
});
79+
3980
it("sample_response with random string in path", async () => {
4081
await recorder.start({ envSetupForPlayback: {} });
4182

sdk/test-utils/recorder/test/utils/server.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,39 @@ app.get("/", (_, res) => {
1515
res.send("Hello world!");
1616
});
1717

18-
app.get("/sample_response", (_, res) => {
18+
app.get("/redirectWithHost", (req, res) => {
19+
res.redirect(307, `http://${req.hostname}:${port}/sample_response`);
20+
});
21+
22+
app.get("/redirectWithoutHost", (_, res) => {
23+
res.redirect(307, `/sample_response`);
24+
});
25+
26+
let sendRetryResponse = true;
27+
28+
app.get("/reset_retry", (_, res) => {
29+
sendRetryResponse = true;
30+
res.send("The retry flag was reset. The next call to /retry will return a 429 status.");
31+
});
32+
33+
app.get("/retry", (_, res) => {
34+
if (sendRetryResponse) {
35+
res
36+
.status(429)
37+
.header("Retry-After", new Date().toUTCString())
38+
.send({ error: "429 Too Many Requests" });
39+
sendRetryResponse = false;
40+
} else {
41+
res.send({ val: "abc" });
42+
}
43+
});
44+
45+
app.get("/sample_response", (req, res) => {
46+
if (req.header("x-recording-id") !== undefined) {
47+
res.status(400).send({ error: "This request bypassed the proxy tool!" });
48+
return;
49+
}
50+
1951
res.send({ val: "abc" });
2052
});
2153

0 commit comments

Comments
 (0)