/
githubmirror
/
amphtml
Обзор
Документация
Войти
/
githubmirror
/
amphtml
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/utils/xhr-utils.js
379 строк
12 KB
Viktor Szépe
Fix typos in src/ (#39606)
12 авг 2025, 21:35
Не верифицирован
12 авг 2025, 21:35
fca692e
Код
Авторство
О чём код?
import {devAssert, userAssert} from '#core/assert'; import {fromIterator, isArray} from '#core/types/array'; import {isObject, map} from '#core/types/object'; import {isExperimentOn} from '#experiments'; import {Services} from '#service'; import {user} from '#utils/log'; import {isFormDataWrapper} from '../form-data-wrapper'; import {getMode} from '../mode'; import { getCorsUrl, getWinOrigin, isProxyOrigin, parseUrlDeprecated, serializeQueryString, } from '../url'; /** @private @const {!Array<string>} */ const allowedMethods_ = ['GET', 'POST']; /** @private @const {!Array<function(*):boolean>} */ const allowedJsonBodyTypes_ = [isArray, isObject]; /** * Serializes a fetch request so that it can be passed to `postMessage()`, * i.e., can be cloned using the * [structured clone algorithm](http://mdn.io/Structured_clone_algorithm). * * The request is serialized in the following way: * * 1. If the `init.body` is a `FormData`, set content-type header to * `multipart/form-data` and transform `init.body` into an * `!Array<!Array<string>>` holding the list of form entries, where each * element in the array is a form entry (key-value pair) represented as a * 2-element array. * * 2. Return a new object having properties `input` and the transformed * `init`. * * The serialized request is assumed to be de-serialized in the following way: * * 1.If content-type header starts with `multipart/form-data` * (case-insensitive), transform the entry array in `init.body` into a * `FormData` object. * * 2. Pass `input` and transformed `init` to `fetch` (or the constructor of * `Request`). * * Currently only `FormData` used in `init.body` is handled as it's the only * type being used in AMP runtime that needs serialization. The `Headers` type * also needs serialization, but callers should not be passing `Headers` * object in `init`, as that fails `fetchPolyfill` on browsers that don't * support fetch. Some serialization-needing types for `init.body` such as * `ArrayBuffer` and `Blob` are already supported by the structured clone * algorithm. Other serialization-needing types such as `URLSearchParams` * (which is not supported in IE and Safari) and `FederatedCredentials` are * not used in AMP runtime. `init.body` can also be a string * (application/x-www-form-urlencoded) but that doesn't require serialization. * * @param {string} input The URL of the XHR to convert to structured * cloneable. * @param {!FetchInitDef} init The options of the XHR to convert to structured * cloneable. * @return {{input: string, init: !FetchInitDef}} The serialized structurally- * cloneable request. */ export function toStructuredCloneable(input, init) { const newInit = /** @type {!FetchInitDef} */ ({...init}); if (isFormDataWrapper(init.body)) { const wrapper = /** @type {!FormDataWrapperInterface} */ (init.body); newInit.headers['Content-Type'] = 'multipart/form-data;charset=utf-8'; newInit.body = fromIterator(wrapper.entries()); } return {input, init: newInit}; } /** * De-serializes a fetch response that was made possible to be passed to * `postMessage()`, i.e., can be cloned using the * [structured clone algorithm](http://mdn.io/Structured_clone_algorithm). * * The response is assumed to be serialized in the following way: * * 1. Transform the entries in the headers of the response into an * `!Array<!Array<string>>` holding the list of header entries, where each * element in the array is a header entry (key-value pair) represented as a * 2-element array. The header key is case-insensitive. * * 2. Include the header entry list and `status` and `statusText` properties * of the response in as `headers`, `status` and `statusText` properties of * `init`. * * 3. Include the body of the response serialized as string in `body`. * * 4. Return a new object having properties `body` and `init`. * * The response is de-serialized in the following way: * * 1. If the `Response` type is supported and `responseType` is not * document, pass `body` and `init` directly to the constructor of `Response`. * * 2. Otherwise, populate a fake XHR object to pass to `FetchResponse` as if * the response is returned by the fetch polyfill. * * 3. If `responseType` is `document`, also parse the body and populate * `responseXML` as a `Document` type. * * @param {JsonObject|string|undefined} response The structurally-cloneable * response to convert back to a regular Response. * @param {string|undefined} responseType The original response type used to * initiate the XHR. * @return {!Response} The deserialized regular response. * @private */ export function fromStructuredCloneable(response, responseType) { userAssert(isObject(response), 'Object expected: %s', response); const isDocumentType = responseType == 'document'; if (!isDocumentType) { // Use native `Response` type if available for performance. If response // type is `document`, we must fall back to `FetchResponse` polyfill // because callers would then rely on the `responseXML` property being // present, which is not supported by the Response type. return new Response(response['body'], response['init']); } const lowercasedHeaders = map(); const data = { status: 200, statusText: 'OK', /** * @param {string} name * @return {string} */ getResponseHeader(name) { return lowercasedHeaders[String(name).toLowerCase()] || null; }, }; if (response['init']) { const init = response['init']; if (isArray(init.headers)) { /** @type {!Array} */ (init.headers).forEach((entry) => { const headerName = entry[0]; const headerValue = entry[1]; lowercasedHeaders[String(headerName).toLowerCase()] = String(headerValue); }); } if (init.status) { data.status = parseInt(init.status, 10); } if (init.statusText) { data.statusText = String(init.statusText); } } return new Response(response['body'] ? String(response['body']) : '', data); } /** * Intercepts the XHR and proxies it through the viewer if necessary. * * XHRs are intercepted if all of the following are true: * - The AMP doc is in single doc mode * - The requested resource is not a 1p request. * - The viewer has the `xhrInterceptor` capability * - The Viewer is a trusted viewer or AMP is currently in development mode * - The AMP doc is opted-in for XHR interception (`<html>` tag has * `allow-xhr-interception` attribute) * * @param {!Window} win * @param {?../service/ampdoc-impl.AmpDoc} ampdocSingle * @param {string} input The URL of the XHR which may get intercepted. * @param {!FetchInitDef} init The options of the XHR which may get * intercepted. * @return {!Promise<!Response|undefined>} * A response returned by the interceptor if XHR is intercepted or * `Promise<undefined>` otherwise. */ export function getViewerInterceptResponse(win, ampdocSingle, input, init) { if (!ampdocSingle) { return Promise.resolve(); } const whenUnblocked = init.prerenderSafe ? Promise.resolve() : ampdocSingle.whenFirstVisible(); const viewer = Services.viewerForDoc(ampdocSingle); const urlIsProxy = isProxyOrigin(input); const viewerCanIntercept = viewer.hasCapability('xhrInterceptor'); const interceptorDisabledForLocalDev = init.bypassInterceptorForDev && getMode(win).localDev; if (urlIsProxy || !viewerCanIntercept || interceptorDisabledForLocalDev) { return whenUnblocked; } const htmlElement = ampdocSingle.getRootNode().documentElement; const docOptedIn = htmlElement.hasAttribute('allow-xhr-interception'); if (!docOptedIn) { return whenUnblocked; } return whenUnblocked .then(() => viewer.isTrustedViewer()) .then((viewerTrusted) => { if ( !( viewerTrusted || getMode(win).localDev || isExperimentOn(win, 'untrusted-xhr-interception') ) ) { return; } const messagePayload = { 'originalRequest': toStructuredCloneable(input, init), }; return viewer .sendMessageAwaitResponse('xhr', messagePayload) .then((response) => fromStructuredCloneable(response, init.responseType) ); }); } /** * Sets up URL based on ampCors * @param {!Window} win * @param {string} input * @param {!FetchInitDef} init The options of the XHR which may get * intercepted. * @return {string} */ export function setupInput(win, input, init) { devAssert(typeof input == 'string', 'Only URL supported: %s', input); if (init.ampCors !== false) { input = getCorsUrl(win, input); } return input; } /** * Sets up and normalizes the FetchInitDef * * @param {?FetchInitDef=} opt_init Fetch options object. * @param {string=} opt_accept The HTTP Accept header value. * @return {!FetchInitDef} */ export function setupInit(opt_init, opt_accept) { const init = opt_init || {}; // In particular, Firefox does not tolerate `null` values for // `credentials`. const creds = init.credentials; devAssert( creds === undefined || creds == 'include' || creds == 'omit', 'Only credentials=include|omit support: %s', creds ); init.method = normalizeMethod_(init.method); init.headers = init.headers || {}; if (opt_accept) { init.headers['Accept'] = opt_accept; } // In edge a `TypeMismatchError` is thrown when body is set to null. devAssert(init.body !== null, 'fetch `body` can not be `null`'); return init; } /** * * Sets up AMPSpecific CORS headers. * @param {!Window} win * @param {string} input * @param {?FetchInitDef=} init * @return {!FetchInitDef} */ export function setupAMPCors(win, input, init) { init = init || {}; // For some same origin requests, add AMP-Same-Origin: true header to allow // publishers to validate that this request came from their own origin. const currentOrigin = getWinOrigin(win); const targetOrigin = parseUrlDeprecated(input).origin; if (currentOrigin == targetOrigin) { init['headers'] = init['headers'] || {}; init['headers']['AMP-Same-Origin'] = 'true'; } return init; } /** * @param {?FetchInitDef=} init * @return {!FetchInitDef} */ export function setupJsonFetchInit(init) { const fetchInit = setupInit(init, 'application/json'); if (fetchInit.method == 'POST' && !isFormDataWrapper(fetchInit.body)) { // Assume JSON strict mode where only objects or arrays are allowed // as body. devAssert( allowedJsonBodyTypes_.some((test) => test(fetchInit.body)), 'body must be of type object or array. %s', fetchInit.body ); // Content should be 'text/plain' to avoid CORS preflight. fetchInit.headers['Content-Type'] = fetchInit.headers['Content-Type'] || 'text/plain;charset=utf-8'; const headerContentType = fetchInit.headers['Content-Type']; // Cast is valid, because we checked that it is not form data above. if (headerContentType === 'application/x-www-form-urlencoded') { fetchInit.body = serializeQueryString( /** @type {!JsonObject} */ (fetchInit.body) ); } else { fetchInit.body = JSON.stringify( /** @type {!JsonObject} */ (fetchInit.body) ); } } return fetchInit; } /** * Normalized method name by uppercasing. * @param {string|undefined} method * @return {string} * @private */ function normalizeMethod_(method) { if (method === undefined) { return 'GET'; } method = method.toUpperCase(); devAssert( allowedMethods_.includes(method), 'Only one of %s is currently allowed. Got %s', allowedMethods_.join(', '), method ); return method; } /** * If 415 or in the 5xx range. * @param {number} status * @return {boolean} */ function isRetriable(status) { return status == 415 || (status >= 500 && status < 600); } /** * Returns the response if successful or otherwise throws an error. * @param {!Response} response * @return {!Promise<!Response>} */ export function assertSuccess(response) { return new Promise((resolve) => { if (response.ok) { return resolve(response); } const {status} = response; const err = user().createError(`HTTP error ${status}`); err['retriable'] = isRetriable(status); // TODO(@jridgewell, #9448): Callers who need the response should // skip processing. err['response'] = response; throw err; }); }