Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions integration/test/ParseServerTest.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
'use strict';

const http = require('http');
const Parse = require('../../node');

describe('ParseServer', () => {
it('can reconfigure server', async () => {
let parseServer = await reconfigureServer({ serverURL: 'www.google.com' });
Expand Down Expand Up @@ -34,4 +37,21 @@ describe('ParseServer', () => {
await object.save();
expect(object.id).toBeDefined();
});

it('can forward redirect', async () => {
const serverURL = Parse.serverURL;
const redirectServer = http.createServer(function(_, res) {
res.writeHead(301, { Location: serverURL });
res.end();
}).listen(8080);
Parse.CoreManager.set('SERVER_URL', 'http://localhost:8080/api');
const object = new TestObject({ foo: 'bar' });
await object.save();
const query = new Parse.Query(TestObject);
const result = await query.get(object.id);
expect(result.id).toBe(object.id);
expect(result.get('foo')).toBe('bar');
Parse.serverURL = serverURL;
redirectServer.close();
});
});
56 changes: 43 additions & 13 deletions src/RESTController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,16 @@ if (typeof XDomainRequest !== 'undefined' && !('withCredentials' in new XMLHttpR
useXDomainRequest = true;
}

function getPath(base: string, pathname: string) {
if (base.endsWith('/')) {
base = base.slice(0, -1);
}
if (!pathname.startsWith('/')) {
pathname = '/' + pathname;
}
return base + pathname;
}

function ajaxIE9(method: string, url: string, data: any, _headers?: any, options?: FullOptions) {
return new Promise((resolve, reject) => {
// @ts-ignore
Expand Down Expand Up @@ -140,6 +150,7 @@ const RESTController = {
method,
headers,
signal,
redirect: 'manual',
};
if (data) {
fetchOptions.body = data;
Expand Down Expand Up @@ -189,6 +200,14 @@ const RESTController = {
} else if (status >= 400 && status < 500) {
const error = await response.json();
promise.reject(error);
} else if ([301, 302, 303, 307, 308].includes(status)) {
const location = response.headers.get('location');
promise.resolve({
status,
location,
method: status === 303 ? 'GET' : method,
dropBody: status === 303,
});
} else if (status >= 500 || status === 0) {
// retry on 5XX or library error
if (++attempts < CoreManager.get('REQUEST_ATTEMPT_LIMIT')) {
Expand Down Expand Up @@ -221,12 +240,7 @@ const RESTController = {

request(method: string, path: string, data: any, options?: RequestOptions) {
options = options || {};
let url = CoreManager.get('SERVER_URL');
if (url[url.length - 1] !== '/') {
url += '/';
}
url += path;

const url = getPath(CoreManager.get('SERVER_URL'), path);
const payload: Partial<PayloadType> = {};
if (data && typeof data === 'object') {
for (const k in data) {
Expand Down Expand Up @@ -302,15 +316,31 @@ const RESTController = {
}

const payloadString = JSON.stringify(payload);
return RESTController.ajax(method, url, payloadString, {}, options).then(
({ response, status, headers }) => {
if (options.returnStatus) {
return { ...response, _status: status, _headers: headers };
} else {
return response;
return RESTController.ajax(method, url, payloadString, {}, options).then(async (result) => {
if (result.location) {
let newURL = getPath(result.location, path);
let newMethod = result.method;
let newBody = result.dropBody ? undefined : payloadString;

// Follow up to 5 redirects to avoid loops
for (let i = 0; i < 5; i += 1) {
const r = await RESTController.ajax(newMethod, newURL, newBody, {}, options);
if (!r.location) {
result = r;
break;
}
newURL = getPath(r.location, path);
newMethod = r.method;
newBody = r.dropBody ? undefined : payloadString;
}
}
);
const { response, status, headers } = result;
if (options.returnStatus) {
return { ...response, _status: status, _headers: headers };
} else {
return response;
}
});
})
.catch(RESTController.handleError);
},
Expand Down
76 changes: 76 additions & 0 deletions src/__tests__/RESTController-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -433,4 +433,80 @@ describe('RESTController', () => {
_SessionToken: '1234',
});
});

it('follows HTTP redirects for batch requests when using a custom SERVER_URL', async () => {
// Configure a reverse-proxy style SERVER_URL
CoreManager.set('SERVER_URL', 'http://test.host/api');

// Prepare a minimal batch payload
const batchData = {
requests: [{
method: 'POST',
path: '/classes/TestObject',
body: { foo: 'bar' }
}]
};

// First response: 301 redirect to /parse/batch; second: successful response
mockFetch(
[
{ status: 301, response: {} },
{ status: 200, response: { success: true } }
],
{ location: 'http://test.host/parse/' }
);

// Issue the batch request
const result = await RESTController.request('POST', 'batch', batchData);

// We expect two fetch calls: one to the original URL, then one to the Location header
expect(fetch.mock.calls.length).toBe(2);
expect(fetch.mock.calls[0][0]).toEqual('http://test.host/api/batch');
expect(fetch.mock.calls[1][0]).toEqual('http://test.host/parse/batch');

// The final result should be the JSON from the second (successful) response
expect(result).toEqual({ success: true });

// Clean up the custom SERVER_URL
CoreManager.set('SERVER_URL', 'https://api.parse.com/1');
});

it('follows multiple HTTP redirects', async () => {
// Configure a reverse-proxy style SERVER_URL
CoreManager.set('SERVER_URL', 'http://test.host/api');

// Prepare a minimal batch payload
const batchData = {
requests: [{
method: 'POST',
path: '/classes/TestObject',
body: { foo: 'bar' }
}]
};

// First response: 301 redirect to /parse/batch; second: successful response
mockFetch(
[
{ status: 301, response: {} },
{ status: 301, response: {} },
{ status: 200, response: { success: true } }
],
{ location: 'http://test.host/parse/' }
);

// Issue the batch request
const result = await RESTController.request('POST', 'batch', batchData);

// We expect three fetch calls: one to the original URL, then two to the Location header
expect(fetch.mock.calls.length).toBe(3);
expect(fetch.mock.calls[0][0]).toEqual('http://test.host/api/batch');
expect(fetch.mock.calls[1][0]).toEqual('http://test.host/parse/batch');
expect(fetch.mock.calls[2][0]).toEqual('http://test.host/parse/batch');

// The final result should be the JSON from the second (successful) response
expect(result).toEqual({ success: true });

// Clean up the custom SERVER_URL
CoreManager.set('SERVER_URL', 'https://api.parse.com/1');
});
});