Skip to content

Commit 2a3b217

Browse files
authored
Merge pull request #1558 from Adyen/disable-308-redirect
Terminal API: option to disable 308 redirect logic
2 parents 482e977 + f3634bb commit 2a3b217

File tree

3 files changed

+76
-29
lines changed

3 files changed

+76
-29
lines changed

src/__tests__/terminalCloudAPI.spec.ts

Lines changed: 55 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import Client from "../client";
66
import TerminalCloudAPI from "../services/terminalCloudAPI";
77
import { terminal } from "../typings";
88
import { EnvironmentEnum } from "../config";
9+
import HttpClientException from "../httpClient/httpClientException";
910

1011
let client: Client;
1112
let terminalCloudAPI: TerminalCloudAPI;
@@ -31,29 +32,29 @@ describe("Terminal Cloud API", (): void => {
3132

3233
const terminalAPIPaymentRequest = createTerminalAPIPaymentRequest();
3334

34-
const requestResponse = await terminalCloudAPI.async(terminalAPIPaymentRequest);
35+
const requestResponse = await terminalCloudAPI.async(terminalAPIPaymentRequest);
3536

36-
expect(typeof requestResponse).toBe("string");
37-
expect(requestResponse).toEqual("ok");
38-
});
37+
expect(typeof requestResponse).toBe("string");
38+
expect(requestResponse).toEqual("ok");
39+
});
3940

40-
test("should get an error after async payment request", async (): Promise<void> => {
41-
scope.post("/async").reply(200, asyncErrorRes);
41+
test("should get an error after async payment request", async (): Promise<void> => {
42+
scope.post("/async").reply(200, asyncErrorRes);
4243

43-
const terminalAPIPaymentRequest = createTerminalAPIPaymentRequest();
44+
const terminalAPIPaymentRequest = createTerminalAPIPaymentRequest();
4445

45-
const requestResponse = await terminalCloudAPI.async(terminalAPIPaymentRequest);
46+
const requestResponse = await terminalCloudAPI.async(terminalAPIPaymentRequest);
4647

47-
if (typeof requestResponse === "object") {
48-
expect(requestResponse.SaleToPOIRequest?.EventNotification).toBeDefined();
49-
expect(requestResponse.SaleToPOIRequest?.EventNotification?.EventToNotify).toBe("Reject");
50-
} else {
51-
throw new Error("Expected structured response, but got raw string");
52-
}
53-
});
48+
if (typeof requestResponse === "object") {
49+
expect(requestResponse.SaleToPOIRequest?.EventNotification).toBeDefined();
50+
expect(requestResponse.SaleToPOIRequest?.EventNotification?.EventToNotify).toBe("Reject");
51+
} else {
52+
throw new Error("Expected structured response, but got raw string");
53+
}
54+
});
5455

55-
test("should make a sync payment request", async (): Promise<void> => {
56-
scope.post("/sync").reply(200, syncRes);
56+
test("should make a sync payment request", async (): Promise<void> => {
57+
scope.post("/sync").reply(200, syncRes);
5758

5859
const terminalAPIPaymentRequest = createTerminalAPIPaymentRequest();
5960
const terminalAPIResponse: terminal.TerminalApiResponse = await terminalCloudAPI.sync(terminalAPIPaymentRequest);
@@ -168,6 +169,7 @@ describe("Terminal Cloud API", (): void => {
168169
const terminalApiHost = "https://terminal-api-test.adyen.com";
169170

170171
const client = new Client({ apiKey: "YOUR_API_KEY", environment: EnvironmentEnum.TEST });
172+
171173
const terminalCloudAPI = new TerminalCloudAPI(client);
172174

173175
const terminalAPIPaymentRequest = createTerminalAPIPaymentRequest();
@@ -191,14 +193,45 @@ describe("Terminal Cloud API", (): void => {
191193
},
192194
});
193195

194-
try {
195-
await terminalCloudAPI.sync(terminalAPIPaymentRequest);
196-
fail("No exception was thrown");
196+
try {
197+
await terminalCloudAPI.sync(terminalAPIPaymentRequest);
198+
fail("No exception was thrown");
197199
} catch (e) {
198-
expect(e).toBeInstanceOf(Error);
200+
expect(e).toBeInstanceOf(Error);
199201
}
200202

201-
});
203+
});
204+
205+
test("async should skip 308 redirect", async (): Promise<void> => {
206+
207+
const terminalApiHost = "https://terminal-api-test.adyen.com";
208+
209+
const client = new Client({ apiKey: "YOUR_API_KEY", environment: EnvironmentEnum.TEST, enable308Redirect: false });
210+
const terminalCloudAPI = new TerminalCloudAPI(client);
211+
212+
const terminalAPIPaymentRequest = createTerminalAPIPaymentRequest();
213+
// custom value to trigger mock 308 response
214+
terminalAPIPaymentRequest.SaleToPOIRequest.MessageHeader.SaleID = "response-with-redirect";
215+
216+
// Mock first request: returns a 308 redirect with Location header
217+
nock(terminalApiHost)
218+
.post("/async", (body) => {
219+
return body?.SaleToPOIRequest?.MessageHeader?.SaleID === "response-with-redirect";
220+
})
221+
.reply(308, "", { Location: `${terminalApiHost}/async?redirect=false` });
222+
223+
224+
// Must throw an error
225+
try {
226+
await terminalCloudAPI.async(terminalAPIPaymentRequest);
227+
fail("No exception was thrown");
228+
} catch (e: unknown) {
229+
expect(e).toBeInstanceOf(HttpClientException);
230+
if (e instanceof HttpClientException) {
231+
expect(e.statusCode).toBe(308);
232+
}
233+
}
234+
});
202235

203236
});
204237

src/config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ interface ConfigConstructor {
4949
terminalApiLocalEndpoint?: string;
5050
liveEndpointUrlPrefix?: string; // must be provided for LIVE integration
5151
region?: RegionEnum; // must be provided for Terminal API integration
52+
enable308Redirect?: boolean; // enabling redirect upon 308 response status
5253
}
5354

5455
const DEFAULT_TIMEOUT = 30000; // Default timeout value (30 sec)
@@ -67,6 +68,8 @@ class Config {
6768
public terminalApiLocalEndpoint?: string;
6869
public liveEndpointUrlPrefix?: string;
6970
public region?: RegionEnum;
71+
public enable308Redirect?: boolean;
72+
7073

7174
public constructor(options: ConfigConstructor = {}) {
7275
if (options.username) this.username = options.username;
@@ -82,6 +85,8 @@ class Config {
8285
if (options.terminalApiLocalEndpoint) this.terminalApiLocalEndpoint = options.terminalApiLocalEndpoint;
8386
if (options.liveEndpointUrlPrefix) this.liveEndpointUrlPrefix = options.liveEndpointUrlPrefix;
8487
if (options.region) this.region = options.region;
88+
this.enable308Redirect = options.enable308Redirect ?? true; // enabled by default
89+
8590
}
8691

8792
/**

src/httpClient/httpURLConnectionClient.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ class HttpURLConnectionClient implements ClientInterface {
8686
requestOptions.headers[ApiConstants.CONTENT_TYPE] = ApiConstants.APPLICATION_JSON_TYPE;
8787

8888
const httpConnection: ClientRequest = this.createRequest(endpoint, requestOptions, config.applicationName);
89-
return this.doRequest(httpConnection, json);
89+
90+
return this.doRequest(httpConnection, json, config.enable308Redirect ?? true);
9091
}
9192

9293
// create Request object
@@ -142,8 +143,15 @@ class HttpURLConnectionClient implements ClientInterface {
142143
return req;
143144
}
144145

145-
// invoke request
146-
private doRequest(connectionRequest: ClientRequest, json: string): Promise<string> {
146+
/**
147+
* Invoke the request
148+
* @param connectionRequest The request
149+
* @param json The payload
150+
* @param allowRedirect Whether to allow redirect upon 308 response status code
151+
* @returns Promise with the API response
152+
*/
153+
private doRequest(connectionRequest: ClientRequest, json: string, allowRedirect: boolean): Promise<string> {
154+
147155
return new Promise((resolve, reject): void => {
148156
connectionRequest.flushHeaders();
149157

@@ -173,8 +181,8 @@ class HttpURLConnectionClient implements ClientInterface {
173181
reject(new Error("The connection was terminated while the message was still being sent"));
174182
}
175183

176-
// Handle 308 redirect
177-
if (res.statusCode && res.statusCode === 308) {
184+
// Handle 308 redirect (when enabled)
185+
if (allowRedirect && res.statusCode && res.statusCode === 308) {
178186
const location = res.headers["location"];
179187
if (location) {
180188
// follow the redirect
@@ -195,7 +203,8 @@ class HttpURLConnectionClient implements ClientInterface {
195203
};
196204
const clientRequestFn = url.protocol === "https:" ? httpsRequest : httpRequest;
197205
const redirectedRequest: ClientRequest = clientRequestFn(newRequestOptions);
198-
const redirectResponse = this.doRequest(redirectedRequest, json);
206+
// To prevent potential redirect loops, disable further redirects for this new request.
207+
const redirectResponse = this.doRequest(redirectedRequest, json, false as boolean);
199208
return resolve(redirectResponse);
200209
} catch (err) {
201210
return reject(err);
@@ -230,7 +239,7 @@ class HttpURLConnectionClient implements ClientInterface {
230239
} catch (e) {
231240
// parsing error
232241
exception = new HttpClientException({
233-
message: `HTTP Exception: ${response.statusCode}. Error parsing response: ${(e as Error).message}`,
242+
message: `HTTP Exception: ${response.statusCode}. Error ${(e as Error).message} while parsing response: ${response.body}`,
234243
statusCode: response.statusCode,
235244
responseHeaders: response.headers,
236245
responseBody: response.body,

0 commit comments

Comments
 (0)