Skip to content

Commit 58e7f58

Browse files
authored
fix: HTTP status code 3XX redirection for Parse Server URL not handled properly (#2608)
1 parent fc416dc commit 58e7f58

File tree

3 files changed

+139
-13
lines changed

3 files changed

+139
-13
lines changed

integration/test/ParseServerTest.js

Lines changed: 20 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,21 @@ describe('ParseServer', () => {
3437
await object.save();
3538
expect(object.id).toBeDefined();
3639
});
40+
41+
it('can forward redirect', async () => {
42+
const serverURL = Parse.serverURL;
43+
const redirectServer = http.createServer(function(_, res) {
44+
res.writeHead(301, { Location: serverURL });
45+
res.end();
46+
}).listen(8080);
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+
redirectServer.close();
56+
});
3757
});

src/RESTController.ts

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

57+
function getPath(base: string, pathname: string) {
58+
if (base.endsWith('/')) {
59+
base = base.slice(0, -1);
60+
}
61+
if (!pathname.startsWith('/')) {
62+
pathname = '/' + pathname;
63+
}
64+
return base + pathname;
65+
}
66+
5767
function ajaxIE9(method: string, url: string, data: any, _headers?: any, options?: FullOptions) {
5868
return new Promise((resolve, reject) => {
5969
// @ts-ignore
@@ -140,6 +150,7 @@ const RESTController = {
140150
method,
141151
headers,
142152
signal,
153+
redirect: 'manual',
143154
};
144155
if (data) {
145156
fetchOptions.body = data;
@@ -189,6 +200,14 @@ const RESTController = {
189200
} else if (status >= 400 && status < 500) {
190201
const error = await response.json();
191202
promise.reject(error);
203+
} else if ([301, 302, 303, 307, 308].includes(status)) {
204+
const location = response.headers.get('location');
205+
promise.resolve({
206+
status,
207+
location,
208+
method: status === 303 ? 'GET' : method,
209+
dropBody: status === 303,
210+
});
192211
} else if (status >= 500 || status === 0) {
193212
// retry on 5XX or library error
194213
if (++attempts < CoreManager.get('REQUEST_ATTEMPT_LIMIT')) {
@@ -221,12 +240,7 @@ const RESTController = {
221240

222241
request(method: string, path: string, data: any, options?: RequestOptions) {
223242
options = options || {};
224-
let url = CoreManager.get('SERVER_URL');
225-
if (url[url.length - 1] !== '/') {
226-
url += '/';
227-
}
228-
url += path;
229-
243+
const url = getPath(CoreManager.get('SERVER_URL'), path);
230244
const payload: Partial<PayloadType> = {};
231245
if (data && typeof data === 'object') {
232246
for (const k in data) {
@@ -302,15 +316,31 @@ const RESTController = {
302316
}
303317

304318
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;
319+
return RESTController.ajax(method, url, payloadString, {}, options).then(async (result) => {
320+
if (result.location) {
321+
let newURL = getPath(result.location, path);
322+
let newMethod = result.method;
323+
let newBody = result.dropBody ? undefined : payloadString;
324+
325+
// Follow up to 5 redirects to avoid loops
326+
for (let i = 0; i < 5; i += 1) {
327+
const r = await RESTController.ajax(newMethod, newURL, newBody, {}, options);
328+
if (!r.location) {
329+
result = r;
330+
break;
331+
}
332+
newURL = getPath(r.location, path);
333+
newMethod = r.method;
334+
newBody = r.dropBody ? undefined : payloadString;
311335
}
312336
}
313-
);
337+
const { response, status, headers } = result;
338+
if (options.returnStatus) {
339+
return { ...response, _status: status, _headers: headers };
340+
} else {
341+
return response;
342+
}
343+
});
314344
})
315345
.catch(RESTController.handleError);
316346
},

src/__tests__/RESTController-test.js

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,4 +433,80 @@ 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', 'https://api.parse.com/1');
472+
});
473+
474+
it('follows multiple HTTP redirects', async () => {
475+
// Configure a reverse-proxy style SERVER_URL
476+
CoreManager.set('SERVER_URL', 'http://test.host/api');
477+
478+
// Prepare a minimal batch payload
479+
const batchData = {
480+
requests: [{
481+
method: 'POST',
482+
path: '/classes/TestObject',
483+
body: { foo: 'bar' }
484+
}]
485+
};
486+
487+
// First response: 301 redirect to /parse/batch; second: successful response
488+
mockFetch(
489+
[
490+
{ status: 301, response: {} },
491+
{ status: 301, response: {} },
492+
{ status: 200, response: { success: true } }
493+
],
494+
{ location: 'http://test.host/parse/' }
495+
);
496+
497+
// Issue the batch request
498+
const result = await RESTController.request('POST', 'batch', batchData);
499+
500+
// We expect three fetch calls: one to the original URL, then two to the Location header
501+
expect(fetch.mock.calls.length).toBe(3);
502+
expect(fetch.mock.calls[0][0]).toEqual('http://test.host/api/batch');
503+
expect(fetch.mock.calls[1][0]).toEqual('http://test.host/parse/batch');
504+
expect(fetch.mock.calls[2][0]).toEqual('http://test.host/parse/batch');
505+
506+
// The final result should be the JSON from the second (successful) response
507+
expect(result).toEqual({ success: true });
508+
509+
// Clean up the custom SERVER_URL
510+
CoreManager.set('SERVER_URL', 'https://api.parse.com/1');
511+
});
436512
});

0 commit comments

Comments
 (0)