-
Notifications
You must be signed in to change notification settings - Fork 2.9k
/
HttpUtils.js
160 lines (144 loc) · 6.28 KB
/
HttpUtils.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
import Onyx from 'react-native-onyx';
import _ from 'underscore';
import CONST from '../CONST';
import ONYXKEYS from '../ONYXKEYS';
import HttpsError from './Errors/HttpsError';
import * as ApiUtils from './ApiUtils';
import alert from '../components/Alert';
let shouldFailAllRequests = false;
let shouldForceOffline = false;
Onyx.connect({
key: ONYXKEYS.NETWORK,
callback: (network) => {
if (!network) {
return;
}
shouldFailAllRequests = Boolean(network.shouldFailAllRequests);
shouldForceOffline = Boolean(network.shouldForceOffline);
},
});
// We use the AbortController API to terminate pending request in `cancelPendingRequests`
let cancellationController = new AbortController();
// To terminate pending ReconnectApp requests https://github.com/Expensify/App/issues/15627
let reconnectAppCancellationController = new AbortController();
/**
* Send an HTTP request, and attempt to resolve the json response.
* If there is a network error, we'll set the application offline.
*
* @param {String} url
* @param {String} [method]
* @param {Object} [body]
* @param {Boolean} [canCancel]
* @param {String} [command]
* @returns {Promise}
*/
function processHTTPRequest(url, method = 'get', body = null, canCancel = true, command = '') {
let signal;
if (canCancel) {
signal = command === CONST.NETWORK.COMMAND.RECONNECT_APP ? reconnectAppCancellationController.signal : cancellationController.signal;
}
return fetch(url, {
// We hook requests to the same Controller signal, so we can cancel them all at once
signal,
method,
body,
})
.then((response) => {
// Test mode where all requests will succeed in the server, but fail to return a response
if (shouldFailAllRequests || shouldForceOffline) {
throw new HttpsError({
message: CONST.ERROR.FAILED_TO_FETCH,
});
}
if (!response.ok) {
// Expensify site is down or there was an internal server error, or something temporary like a Bad Gateway, or unknown error occurred
const serviceInterruptedStatuses = [
CONST.HTTP_STATUS.INTERNAL_SERVER_ERROR,
CONST.HTTP_STATUS.BAD_GATEWAY,
CONST.HTTP_STATUS.GATEWAY_TIMEOUT,
CONST.HTTP_STATUS.UNKNOWN_ERROR,
];
if (_.contains(serviceInterruptedStatuses, response.status)) {
throw new HttpsError({
message: CONST.ERROR.EXPENSIFY_SERVICE_INTERRUPTED,
status: response.status,
title: 'Issue connecting to Expensify site',
});
} else if (response.status === CONST.HTTP_STATUS.TOO_MANY_REQUESTS) {
throw new HttpsError({
message: CONST.ERROR.THROTTLED,
status: response.status,
title: 'API request throttled',
});
}
throw new HttpsError({
message: response.statusText,
status: response.status,
});
}
return response.json();
})
.then((response) => {
// Some retried requests will result in a "Unique Constraints Violation" error from the server, which just means the record already exists
if (response.jsonCode === CONST.JSON_CODE.BAD_REQUEST && response.message === CONST.ERROR_TITLE.DUPLICATE_RECORD) {
throw new HttpsError({
message: CONST.ERROR.DUPLICATE_RECORD,
status: CONST.JSON_CODE.BAD_REQUEST,
title: CONST.ERROR_TITLE.DUPLICATE_RECORD,
});
}
// Auth is down or timed out while making a request
if (response.jsonCode === CONST.JSON_CODE.EXP_ERROR && response.title === CONST.ERROR_TITLE.SOCKET && response.type === CONST.ERROR_TYPE.SOCKET) {
throw new HttpsError({
message: CONST.ERROR.EXPENSIFY_SERVICE_INTERRUPTED,
status: CONST.JSON_CODE.EXP_ERROR,
title: CONST.ERROR_TITLE.SOCKET,
});
}
if (response.jsonCode === CONST.JSON_CODE.MANY_WRITES_ERROR) {
const {phpCommandName, authWriteCommands} = response.data;
// eslint-disable-next-line max-len
const message = `The API call (${phpCommandName}) did more Auth write requests than allowed. Count ${authWriteCommands.length}, commands: ${authWriteCommands.join(
', ',
)}. Check the APIWriteCommands class in Web-Expensify`;
alert('Too many auth writes', message);
}
return response;
});
}
/**
* Makes XHR request
* @param {String} command the name of the API command
* @param {Object} data parameters for the API command
* @param {String} type HTTP request type (get/post)
* @param {Boolean} shouldUseSecure should we use the secure server
* @returns {Promise}
*/
function xhr(command, data, type = CONST.NETWORK.METHOD.POST, shouldUseSecure = false) {
const formData = new FormData();
_.each(data, (val, key) => {
// Do not send undefined request parameters to our API. They will be processed as strings of 'undefined'.
if (_.isUndefined(val)) {
return;
}
formData.append(key, val);
});
const url = ApiUtils.getCommandURL({shouldUseSecure, command});
return processHTTPRequest(url, type, formData, data.canCancel, command);
}
function cancelPendingReconnectAppRequest() {
reconnectAppCancellationController.abort();
reconnectAppCancellationController = new AbortController();
}
function cancelPendingRequests() {
cancellationController.abort();
// We create a new instance because once `abort()` is called any future requests using the same controller would
// automatically get rejected: https://dom.spec.whatwg.org/#abortcontroller-api-integration
cancellationController = new AbortController();
cancelPendingReconnectAppRequest();
}
export default {
xhr,
cancelPendingRequests,
cancelPendingReconnectAppRequest,
};