Skip to content

Commit eaddb1b

Browse files
committed
feat: Properly handle proxy redirection
1 parent fc416dc commit eaddb1b

File tree

3 files changed

+79
-14
lines changed

3 files changed

+79
-14
lines changed

integration/test/ParseServerTest.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
'use strict';
22

3+
const http = require('http');
4+
const Parse = require('../../node');
5+
36
describe('ParseServer', () => {
47
it('can reconfigure server', async () => {
58
let parseServer = await reconfigureServer({ serverURL: 'www.google.com' });
@@ -34,4 +37,20 @@ describe('ParseServer', () => {
3437
await object.save();
3538
expect(object.id).toBeDefined();
3639
});
40+
41+
it('can forward redirect', async () => {
42+
http.createServer(function(_, res) {
43+
res.writeHead(301, { Location: 'http://localhost:1337/parse' });
44+
res.end();
45+
}).listen(8080);
46+
const serverURL = Parse.serverURL;
47+
Parse.CoreManager.set('SERVER_URL', 'http://localhost:8080/api');
48+
const object = new TestObject({ foo: 'bar' });
49+
await object.save();
50+
const query = new Parse.Query(TestObject);
51+
const result = await query.get(object.id);
52+
expect(result.id).toBe(object.id);
53+
expect(result.get('foo')).toBe('bar');
54+
Parse.serverURL = serverURL;
55+
});
3756
});

src/RESTController.ts

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@ if (typeof XDomainRequest !== 'undefined' && !('withCredentials' in new XMLHttpR
5454
useXDomainRequest = true;
5555
}
5656

57+
function getPath(url: string, path: string) {
58+
if (url[url.length - 1] !== '/') {
59+
url += '/';
60+
}
61+
return url + path;
62+
}
63+
5764
function ajaxIE9(method: string, url: string, data: any, _headers?: any, options?: FullOptions) {
5865
return new Promise((resolve, reject) => {
5966
// @ts-ignore
@@ -140,6 +147,7 @@ const RESTController = {
140147
method,
141148
headers,
142149
signal,
150+
redirect: 'manual',
143151
};
144152
if (data) {
145153
fetchOptions.body = data;
@@ -189,6 +197,9 @@ const RESTController = {
189197
} else if (status >= 400 && status < 500) {
190198
const error = await response.json();
191199
promise.reject(error);
200+
} else if (status === 301 || status === 302 || status === 303 || status === 307) {
201+
const location = response.headers.get('location');
202+
promise.resolve({ status, location, method: status === 303 ? 'GET' : method });
192203
} else if (status >= 500 || status === 0) {
193204
// retry on 5XX or library error
194205
if (++attempts < CoreManager.get('REQUEST_ATTEMPT_LIMIT')) {
@@ -221,12 +232,7 @@ const RESTController = {
221232

222233
request(method: string, path: string, data: any, options?: RequestOptions) {
223234
options = options || {};
224-
let url = CoreManager.get('SERVER_URL');
225-
if (url[url.length - 1] !== '/') {
226-
url += '/';
227-
}
228-
url += path;
229-
235+
const url = getPath(CoreManager.get('SERVER_URL'), path);
230236
const payload: Partial<PayloadType> = {};
231237
if (data && typeof data === 'object') {
232238
for (const k in data) {
@@ -302,15 +308,18 @@ const RESTController = {
302308
}
303309

304310
const payloadString = JSON.stringify(payload);
305-
return RESTController.ajax(method, url, payloadString, {}, options).then(
306-
({ response, status, headers }) => {
307-
if (options.returnStatus) {
308-
return { ...response, _status: status, _headers: headers };
309-
} else {
310-
return response;
311-
}
311+
return RESTController.ajax(method, url, payloadString, {}, options).then(async (result) => {
312+
if (result.location) {
313+
const newURL = getPath(result.location, path);
314+
result = await RESTController.ajax(result.method, newURL, payloadString, {}, options);
315+
}
316+
const { response, status, headers } = result;
317+
if (options.returnStatus) {
318+
return { ...response, _status: status, _headers: headers };
319+
} else {
320+
return response;
312321
}
313-
);
322+
});
314323
})
315324
.catch(RESTController.handleError);
316325
},

src/__tests__/RESTController-test.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,4 +433,41 @@ describe('RESTController', () => {
433433
_SessionToken: '1234',
434434
});
435435
});
436+
437+
it('follows HTTP redirects for batch requests when using a custom SERVER_URL', async () => {
438+
// Configure a reverse-proxy style SERVER_URL
439+
CoreManager.set('SERVER_URL', 'http://test.host/api');
440+
441+
// Prepare a minimal batch payload
442+
const batchData = {
443+
requests: [{
444+
method: 'POST',
445+
path: '/classes/TestObject',
446+
body: { foo: 'bar' }
447+
}]
448+
};
449+
450+
// First response: 301 redirect to /parse/batch; second: successful response
451+
mockFetch(
452+
[
453+
{ status: 301, response: {} },
454+
{ status: 200, response: { success: true } }
455+
],
456+
{ location: 'http://test.host/parse/' }
457+
);
458+
459+
// Issue the batch request
460+
const result = await RESTController.request('POST', 'batch', batchData);
461+
462+
// We expect two fetch calls: one to the original URL, then one to the Location header
463+
expect(fetch.mock.calls.length).toBe(2);
464+
expect(fetch.mock.calls[0][0]).toEqual('http://test.host/api/batch');
465+
expect(fetch.mock.calls[1][0]).toEqual('http://test.host/parse/batch');
466+
467+
// The final result should be the JSON from the second (successful) response
468+
expect(result).toEqual({ success: true });
469+
470+
// Clean up the custom SERVER_URL
471+
CoreManager.set('SERVER_URL', undefined);
472+
});
436473
});

0 commit comments

Comments
 (0)