diff --git a/src/network-layer/default/RelayDefaultNetworkLayer.js b/src/network-layer/default/RelayDefaultNetworkLayer.js index df67d0cc5ff9a..5a548e7b43e64 100644 --- a/src/network-layer/default/RelayDefaultNetworkLayer.js +++ b/src/network-layer/default/RelayDefaultNetworkLayer.js @@ -12,7 +12,7 @@ 'use strict'; -import type RelayMutationRequest from 'RelayMutationRequest'; +const RelayMutationRequest = require('RelayMutationRequest'); import type RelayQueryRequest from 'RelayQueryRequest'; const fetch = require('fetch'); @@ -47,12 +47,7 @@ class RelayDefaultNetworkLayer { result => result.json() ).then(payload => { if (payload.hasOwnProperty('errors')) { - const error = new Error( - 'Server request for mutation `' + request.getDebugName() + '` ' + - 'failed for the following reasons:\n\n' + - formatRequestErrors(request, payload.errors) - ); - (error: any).source = payload; + const error = createRequestError(request, '200', payload); request.reject(error); } else { request.resolve({response: payload.data}); @@ -68,12 +63,7 @@ class RelayDefaultNetworkLayer { result => result.json() ).then(payload => { if (payload.hasOwnProperty('errors')) { - const error = new Error( - 'Server request for query `' + request.getDebugName() + '` ' + - 'failed for the following reasons:\n\n' + - formatRequestErrors(request, payload.errors) - ); - (error: any).source = payload; + const error = createRequestError(request, '200', payload); request.reject(error); } else if (!payload.hasOwnProperty('data')) { request.reject(new Error( @@ -132,7 +122,7 @@ class RelayDefaultNetworkLayer { method: 'POST', }; } - return fetch(this._uri, init).then(throwOnServerError); + return fetch(this._uri, init).then(throwOnServerError.bind(this, request)); } /** @@ -159,11 +149,16 @@ class RelayDefaultNetworkLayer { * Rejects HTTP responses with a status code that is not >= 200 and < 300. * This is done to follow the internal behavior of `fetchWithRetries`. */ -function throwOnServerError(response: any): any { +function throwOnServerError( + request: RelayMutationRequest, + response: any +): any { if (response.status >= 200 && response.status < 300) { return response; } else { - throw response; + return response.text().then(payload => { + throw createRequestError(request, response.status, payload); + }); } } @@ -199,4 +194,21 @@ function formatRequestErrors( }).join('\n'); } +function createRequestError( + request: RelayMutationRequest | RelayQueryRequest, + responseStatus: string, + payload: any +): Error { + const requestType: string = request instanceof RelayMutationRequest ? 'mutation' : 'query'; + const errorReason: string = typeof payload === 'object' ? formatRequestErrors(request, payload.errors) : + 'Server response had an error status: ' + responseStatus; + + const error = new Error('Server request for ' + requestType + ' `' + request.getDebugName() + '` ' + + 'failed for the following reasons:\n\n' + errorReason); + (error: any).source = payload; + (error: any).status = responseStatus; + + return error; +} + module.exports = RelayDefaultNetworkLayer; diff --git a/src/network-layer/default/__tests__/RelayDefaultNetworkLayer-test.js b/src/network-layer/default/__tests__/RelayDefaultNetworkLayer-test.js index f8a9aaa965df6..210adf3416008 100644 --- a/src/network-layer/default/__tests__/RelayDefaultNetworkLayer-test.js +++ b/src/network-layer/default/__tests__/RelayDefaultNetworkLayer-test.js @@ -36,6 +36,14 @@ describe('RelayDefaultNetworkLayer', () => { }; } + function genFailureResponse(data) { + return { + json: () => Promise.resolve(data), + text: () => Promise.resolve(JSON.stringify(data)), + status: 500, + }; + } + beforeEach(() => { jest.resetModuleRegistry(); @@ -172,6 +180,7 @@ describe('RelayDefaultNetworkLayer', () => { ' ^^^', ].join('\n')); expect(error.source).toEqual(response); + expect(error.status).toEqual('200'); }); it('handles errors with column 0', () => { @@ -201,6 +210,7 @@ describe('RelayDefaultNetworkLayer', () => { ' ^^^', ].join('\n')); expect(error.source).toEqual(response); + expect(error.status).toEqual('200'); }); it('handles custom errors', () => { @@ -227,6 +237,36 @@ describe('RelayDefaultNetworkLayer', () => { '1. Something went wrong.', ].join('\n')); expect(error.source).toEqual(response); + expect(error.status).toEqual('200'); + }); + + it('handles server-side non-2xx errors', () => { + const response = { + errors: [{ + message: 'Something went completely wrong.' + }], + }; + + expect(fetch).not.toBeCalled(); + networkLayer.sendMutation(request); + expect(fetch).toBeCalled(); + const failureResponse = genFailureResponse(response); + + fetch.mock.deferreds[0].resolve(failureResponse); + jest.runAllTimers(); + + expect(rejectCallback.mock.calls.length).toBe(1); + const error = rejectCallback.mock.calls[0][0]; + expect(error instanceof Error).toBe(true); + expect(error.message).toEqual([ + 'Server request for mutation \`FeedbackLikeMutation\` failed for the ' + + 'following reasons:', + '', + 'Server response had an error status: 500', + ].join('\n')); + expect(error.status).toEqual(failureResponse.status); + expect(error.source).toBe('{"errors":[{"message":"Something went ' + + 'completely wrong."}]}'); }); }); @@ -347,6 +387,7 @@ describe('RelayDefaultNetworkLayer', () => { ' ^^^', ].join('\n')); expect(error.source).toEqual(payloadA); + expect(error.status).toEqual('200'); }); it('rejects requests with missing responses', () => {