-
Notifications
You must be signed in to change notification settings - Fork 3.9k
/
xhr-impl.js
219 lines (202 loc) · 6.1 KB
/
xhr-impl.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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
import {parseJson} from '#core/types/object/json';
import {Services} from '#service';
import {dev, user} from '#utils/log';
import {
assertSuccess,
getViewerInterceptResponse,
setupAMPCors,
setupInit,
setupInput,
setupJsonFetchInit,
} from '#utils/xhr-utils';
import {isFormDataWrapper} from '../form-data-wrapper';
import {getService, registerServiceBuilder} from '../service-helpers';
import {getCorsUrl, parseUrlDeprecated} from '../url';
/**
* A service that polyfills Fetch API for use within AMP.
*
* @package Visible for type.
* @visibleForTesting
*/
export class Xhr {
/**
* @param {!Window} win
*/
constructor(win) {
/** @const {!Window} */
this.win = win;
const ampdocService = Services.ampdocServiceFor(win);
// The isSingleDoc check is required because if in shadow mode, this will
// throw a console error because the shellShadowDoc_ is not set when
// fetching the amp doc. So either the test-bind-impl or test pre setup in
// shadow mode tests needs to be fixed or there is a bug in ampdoc impl
// getAmpDoc.
// TODO(alabiaga): This should be investigated and fixed
/** @private {?./ampdoc-impl.AmpDoc} */
this.ampdocSingle_ = ampdocService.isSingleDoc()
? ampdocService.getSingleDoc()
: null;
}
/**
* We want to call `fetch_` unbound from any context since it could
* be either the native fetch or our polyfill.
*
* @param {string} input
* @param {!FetchInitDef} init
* @return {!Promise<!Response>}
* @private
*/
fetch_(input, init) {
return getViewerInterceptResponse(
this.win,
this.ampdocSingle_,
input,
init
).then((interceptorResponse) => {
if (interceptorResponse) {
return interceptorResponse;
}
// After this point, both the native `fetch` and the `fetch` polyfill
// will expect a native `FormData` object in the `body` property, so
// the native `FormData` object needs to be unwrapped.
if (isFormDataWrapper(init.body)) {
const formDataWrapper = /** @type {!FormDataWrapperInterface} */ (
init.body
);
init.body = formDataWrapper.getFormData();
}
return this.win.fetch.apply(null, arguments);
});
}
/**
* Performs the final initialization and requests the fetch. It does two
* main things:
* - It adds "__amp_source_origin" URL parameter with source origin
* USE WITH CAUTION: setting ampCors to false disables AMP source origin check
* but allows for caching resources cross pages.
*
* @param {string} input
* @param {!FetchInitDef=} init
* @return {!Promise<!Response>}
* @private
*/
fetchAmpCors_(input, init = {}) {
input = setupInput(this.win, input, init);
init = setupAMPCors(this.win, input, init);
return this.fetch_(input, init).then(
(response) => response,
(reason) => {
const targetOrigin = parseUrlDeprecated(input).origin;
throw user().createExpectedError(
'XHR',
`Failed fetching (${targetOrigin}/...):`,
reason && /** @type {!Error} */ (reason).message
);
}
);
}
/**
* Fetches a JSON response. Note this returns the response object, not the
* response's JSON. #fetchJson merely sets up the request to accept JSON.
*
* See https://developer.mozilla.org/en-US/docs/Web/API/GlobalFetch/fetch
*
* See `fetchAmpCors_` for more detail.
*
* @param {string} input
* @param {?FetchInitDef=} opt_init
* @return {!Promise<!Response>}
*/
fetchJson(input, opt_init) {
return this.fetch(input, setupJsonFetchInit(opt_init));
}
/**
* Fetches a text response. Note this returns the response object, not the
* response's text. #fetchText merely sets up the request to accept text.
*
* See https://developer.mozilla.org/en-US/docs/Web/API/GlobalFetch/fetch
*
* See `fetchAmpCors_` for more detail.
*
* @param {string} input
* @param {?FetchInitDef=} opt_init
* @return {!Promise<!Response>}
*/
fetchText(input, opt_init) {
return this.fetch(input, setupInit(opt_init, 'text/plain'));
}
/**
* A subsitute for the standard response.json(), which may optionally strip a prefix before calling JSON.parse().
*
* @param {!Response} res fetch response to convert to json.
* @param {string|undefined} prefix to strip away.
* @return {Promise<*>}
*/
xssiJson(res, prefix) {
if (!prefix) {
return res.json();
}
return res.text().then((txt) => {
if (!txt.startsWith(dev().assertString(prefix))) {
user().warn(
'XHR',
`Failed to strip missing prefix "${prefix}" in fetch response.`
);
return parseJson(txt);
}
return parseJson(txt.slice(prefix.length));
});
}
/**
* @param {string} input URL
* @param {?FetchInitDef=} opt_init Fetch options object.
* @return {!Promise<!Response>}
*/
fetch(input, opt_init) {
const init = setupInit(opt_init);
return this.fetchAmpCors_(input, init).then((response) =>
assertSuccess(response)
);
}
/**
* Sends the request, awaits result and confirms that it was successful.
*
* See https://developer.mozilla.org/en-US/docs/Web/API/GlobalFetch/fetch
*
* See `fetchAmpCors_` for more detail.
*
* @param {string} input
* @param {!FetchInitDef=} opt_init
* @return {!Promise}
*/
sendSignal(input, opt_init) {
return this.fetchAmpCors_(input, opt_init).then((response) =>
assertSuccess(response)
);
}
/**
* Add "__amp_source_origin" query parameter to the URL. Ideally, we'd be
* able to set a header (e.g. AMP-Source-Origin), but this will force
* preflight request on all CORS request.
* @param {!Window} win
* @param {string} url
* @return {string}
*/
getCorsUrl(win, url) {
return getCorsUrl(win, url);
}
}
/**
* @param {!Window} window
* @return {!Xhr}
*/
export function xhrServiceForTesting(window) {
installXhrService(window);
return getService(window, 'xhr');
}
/**
* @param {!Window} window
*/
export function installXhrService(window) {
registerServiceBuilder(window, 'xhr', Xhr);
}