in src/amo/api/index.js [89:230]
export function callApi({
endpoint,
params = {},
auth = false,
apiState = initialApiState,
method = 'GET',
body,
credentials,
errorHandler,
_config = config,
version = _config.get('apiVersion'),
_log = log,
}: CallApiParams): Promise<Object> {
if (!endpoint) {
return Promise.reject(
new Error(`endpoint URL cannot be falsy: "${endpoint}"`),
);
}
if (errorHandler) {
errorHandler.clear();
}
const apiPath = `${config.get('apiPath')}${version}`;
const parsedUrl = url.parse(endpoint, true);
let adjustedEndpoint = parsedUrl.pathname || '';
if (!parsedUrl.host) {
// If it's a relative URL, add the API prefix.
const slash = !adjustedEndpoint.startsWith('/') ? '/' : '';
adjustedEndpoint = `${apiPath}${slash}${adjustedEndpoint}`;
} else if (!adjustedEndpoint.startsWith(apiPath)) {
// If it's an absolute URL, it must have the correct prefix.
return Promise.reject(
new Error(`Absolute URL "${endpoint}" has an unexpected prefix.`),
);
}
// Preserve the original query string if there is one.
// This might happen when we parse `next` URLs returned by the API.
const queryString = makeQueryString({
...parsedUrl.query,
...params,
lang: apiState.lang,
});
const options = {
headers: {},
// Always make sure the method is upper case so that the browser won't
// complain about CORS problems.
method: method.toUpperCase(),
credentials: undefined,
body: undefined,
};
if (credentials) {
options.credentials = 'include';
}
if (body) {
if (body instanceof FormData) {
options.body = body;
// Let the browser sets this header, including the boundary value.
// $FlowIgnore
delete options.headers['Content-type'];
} else {
options.body = JSON.stringify(body);
options.headers['Content-type'] = 'application/json';
}
}
if (auth) {
if (apiState.token) {
options.headers.authorization = `Session ${apiState.token}`;
}
}
if (apiState.regionCode) {
options.headers[REGION_CODE_HEADER] = apiState.regionCode;
}
adjustedEndpoint = adjustedEndpoint.endsWith('/')
? adjustedEndpoint
: `${adjustedEndpoint}/`;
const apiURL = `${config.get('apiHost')}${adjustedEndpoint}${queryString}`;
// Flow expects headers['Content-type'] to be a string, but we sometimes
// delete it at line 148, above.
// $FlowIgnore
return fetch(apiURL, options)
.then((response) => {
// There isn't always a 'Content-Type' in headers, e.g., with a DELETE
// method or 5xx responses.
let contentType = response.headers.get('Content-Type');
contentType = contentType && contentType.toLowerCase();
// This is a bit paranoid, but we ensure the API returns a JSON response
// (see https://github.com/mozilla/addons-frontend/issues/1701).
// If not we'll store the text response in JSON and log an error.
// If the JSON parsing fails; we log the error and return an "unknown
// error".
if (contentType === 'application/json') {
return response
.json()
.then((jsonResponse) => ({ response, jsonResponse }));
}
return response.text().then((text) => {
// eslint-disable-next-line amo/only-log-strings
_log.warn(
oneLine`Response from API was not JSON (was Content-Type:
${contentType}) %o`,
{
body: text ? text.substring(0, 100) : '[empty]',
status: response.status || '[unknown]',
url: response.url || '[unknown]',
},
);
// jsonResponse should be an empty object in this case. Otherwise, its
// keys could be treated as generic API errors.
return { jsonResponse: {}, response };
});
})
.then(
({ response, jsonResponse }) => {
if (response.ok) {
return jsonResponse;
}
// If response is not ok we'll throw an error.
const apiError = createApiError({ apiURL, response, jsonResponse });
if (errorHandler) {
errorHandler.handle(apiError);
}
throw apiError;
},
(fetchError) => {
// This actually handles the case when the call to fetch() is
// rejected, say, for a network connection error, etc.
if (errorHandler) {
errorHandler.handle(fetchError);
}
throw fetchError;
},
);
}