@remix-run/router 1.3.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # `@remix-run/router`
2
2
 
3
+ ## 1.3.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Fixes 2 separate issues for revalidating fetcher `shouldRevalidate` calls ([#9948](https://github.com/remix-run/react-router/pull/9948))
8
+ - The `shouldRevalidate` function was only being called for _explicit_ revalidation scenarios (after a mutation, manual `useRevalidator` call, or an `X-Remix-Revalidate` header used for cookie setting in Remix). It was not properly being called on _implicit_ revalidation scenarios that also apply to navigation `loader` revalidation, such as a change in search params or clicking a link for the page we're already on. It's now correctly called in those additional scenarios.
9
+ - The parameters being passed were incorrect and inconsistent with one another since the `current*`/`next*` parameters reflected the static `fetcher.load` URL (and thus were identical). Instead, they should have reflected the the navigation that triggered the revalidation (as the `form*` parameters did). These parameters now correctly reflect the triggering navigation.
10
+ - Respect `preventScrollReset` on `<fetcher.Form>` ([#9963](https://github.com/remix-run/react-router/pull/9963))
11
+ - Do not short circuit on hash change only mutation submissions ([#9944](https://github.com/remix-run/react-router/pull/9944))
12
+ - Remove `instanceof` check from `isRouteErrorResponse` to avoid bundling issues on the server ([#9930](https://github.com/remix-run/react-router/pull/9930))
13
+ - Fix navigation for hash routers on manual URL changes ([#9980](https://github.com/remix-run/react-router/pull/9980))
14
+ - Detect when a `defer` call only contains critical data and remove the `AbortController` ([#9965](https://github.com/remix-run/react-router/pull/9965))
15
+ - Send the name as the value when url-encoding `File` `FormData` entries ([#9867](https://github.com/remix-run/react-router/pull/9867))
16
+
3
17
  ## 1.3.0
4
18
 
5
19
  ### Minor Changes
package/dist/history.d.ts CHANGED
@@ -71,7 +71,7 @@ export interface Update {
71
71
  /**
72
72
  * The delta between this location and the former location in the history stack
73
73
  */
74
- delta: number;
74
+ delta: number | null;
75
75
  }
76
76
  /**
77
77
  * A function that receives notifications about location changes.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @remix-run/router v1.3.0
2
+ * @remix-run/router v1.3.1
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -433,26 +433,17 @@ function getUrlBasedHistory(getLocation, createHref, validateLocation, options)
433
433
  }
434
434
 
435
435
  function handlePop() {
436
- let nextAction = exports.Action.Pop;
436
+ action = exports.Action.Pop;
437
437
  let nextIndex = getIndex();
438
+ let delta = nextIndex == null ? null : nextIndex - index;
439
+ index = nextIndex;
438
440
 
439
- if (nextIndex != null) {
440
- let delta = nextIndex - index;
441
- action = nextAction;
442
- index = nextIndex;
443
-
444
- if (listener) {
445
- listener({
446
- action,
447
- location: history.location,
448
- delta
449
- });
450
- }
451
- } else {
452
- warning$1(false, // TODO: Write up a doc that explains our blocking strategy in detail
453
- // and link to it here so people can understand better what is going on
454
- // and how to avoid it.
455
- "You are trying to block a POP navigation to a location that was not " + "created by @remix-run/router. The block will fail silently in " + "production, but in general you should do all navigation with the " + "router (instead of using window.history.pushState directly) " + "to avoid this situation.");
441
+ if (listener) {
442
+ listener({
443
+ action,
444
+ location: history.location,
445
+ delta
446
+ });
456
447
  }
457
448
  }
458
449
 
@@ -1250,6 +1241,12 @@ class DeferredData {
1250
1241
  [key]: this.trackPromise(key, value)
1251
1242
  });
1252
1243
  }, {});
1244
+
1245
+ if (this.done) {
1246
+ // All incoming values were resolved
1247
+ this.unlistenAbortSignal();
1248
+ }
1249
+
1253
1250
  this.init = responseInit;
1254
1251
  }
1255
1252
 
@@ -1437,11 +1434,11 @@ class ErrorResponse {
1437
1434
  }
1438
1435
  /**
1439
1436
  * Check if the given error is an ErrorResponse generated from a 4xx/5xx
1440
- * Response throw from an action/loader
1437
+ * Response thrown from an action/loader
1441
1438
  */
1442
1439
 
1443
- function isRouteErrorResponse(e) {
1444
- return e instanceof ErrorResponse;
1440
+ function isRouteErrorResponse(error) {
1441
+ return error != null && typeof error.status === "number" && typeof error.statusText === "string" && typeof error.internal === "boolean" && "data" in error;
1445
1442
  }
1446
1443
 
1447
1444
  //#region Types and Constants
@@ -1616,13 +1613,14 @@ function createRouter(init) {
1616
1613
  return;
1617
1614
  }
1618
1615
 
1616
+ warning(activeBlocker != null && delta === null, "You are trying to use a blocker on a POP navigation to a location " + "that was not created by @remix-run/router. This will fail silently in " + "production. This can happen if you are navigating outside the router " + "via `window.history.pushState`/`window.location.hash` instead of using " + "router navigation APIs. This can also happen if you are using " + "createHashRouter and the user manually changes the URL.");
1619
1617
  let blockerKey = shouldBlockNavigation({
1620
1618
  currentLocation: state.location,
1621
1619
  nextLocation: location,
1622
1620
  historyAction
1623
1621
  });
1624
1622
 
1625
- if (blockerKey) {
1623
+ if (blockerKey && delta != null) {
1626
1624
  // Restore the URL to match the current UI, but don't update router state
1627
1625
  ignoreNextHistoryUpdate = true;
1628
1626
  init.history.go(delta * -1); // Put the blocker into a blocked state
@@ -1905,10 +1903,12 @@ function createRouter(init) {
1905
1903
  }
1906
1904
  });
1907
1905
  return;
1908
- } // Short circuit if it's only a hash change
1906
+ } // Short circuit if it's only a hash change and not a mutation submission
1907
+ // For example, on /page#hash and submit a <Form method="post"> which will
1908
+ // default to a navigation to /page
1909
1909
 
1910
1910
 
1911
- if (isHashChangeOnly(state.location, location)) {
1911
+ if (isHashChangeOnly(state.location, location) && !(opts && opts.submission && isMutationMethod(opts.submission.formMethod))) {
1912
1912
  completeNavigation(location, {
1913
1913
  matches
1914
1914
  });
@@ -2122,9 +2122,8 @@ function createRouter(init) {
2122
2122
 
2123
2123
 
2124
2124
  if (!isUninterruptedRevalidation) {
2125
- revalidatingFetchers.forEach(_ref2 => {
2126
- let [key] = _ref2;
2127
- let fetcher = state.fetchers.get(key);
2125
+ revalidatingFetchers.forEach(rf => {
2126
+ let fetcher = state.fetchers.get(rf.key);
2128
2127
  let revalidatingFetcher = {
2129
2128
  state: "loading",
2130
2129
  data: fetcher && fetcher.data,
@@ -2134,7 +2133,7 @@ function createRouter(init) {
2134
2133
  formData: undefined,
2135
2134
  " _hasFetcherDoneAnything ": true
2136
2135
  };
2137
- state.fetchers.set(key, revalidatingFetcher);
2136
+ state.fetchers.set(rf.key, revalidatingFetcher);
2138
2137
  });
2139
2138
  let actionData = pendingActionData || state.actionData;
2140
2139
  updateState(_extends({
@@ -2149,10 +2148,7 @@ function createRouter(init) {
2149
2148
  }
2150
2149
 
2151
2150
  pendingNavigationLoadId = ++incrementingLoadId;
2152
- revalidatingFetchers.forEach(_ref3 => {
2153
- let [key] = _ref3;
2154
- return fetchControllers.set(key, pendingNavigationController);
2155
- });
2151
+ revalidatingFetchers.forEach(rf => fetchControllers.set(rf.key, pendingNavigationController));
2156
2152
  let {
2157
2153
  results,
2158
2154
  loaderResults,
@@ -2168,10 +2164,7 @@ function createRouter(init) {
2168
2164
  // reassigned to new controllers for the next navigation
2169
2165
 
2170
2166
 
2171
- revalidatingFetchers.forEach(_ref4 => {
2172
- let [key] = _ref4;
2173
- return fetchControllers.delete(key);
2174
- }); // If any loaders returned a redirect Response, start a new REPLACE navigation
2167
+ revalidatingFetchers.forEach(rf => fetchControllers.delete(rf.key)); // If any loaders returned a redirect Response, start a new REPLACE navigation
2175
2168
 
2176
2169
  let redirect = findRedirect(results);
2177
2170
 
@@ -2235,6 +2228,7 @@ function createRouter(init) {
2235
2228
  submission
2236
2229
  } = normalizeNavigateOptions(href, opts, true);
2237
2230
  let match = getTargetMatch(matches, path);
2231
+ pendingPreventScrollReset = (opts && opts.preventScrollReset) === true;
2238
2232
 
2239
2233
  if (submission && isMutationMethod(submission.formMethod)) {
2240
2234
  handleFetcherAction(key, routeId, path, match, matches, submission);
@@ -2243,7 +2237,12 @@ function createRouter(init) {
2243
2237
  // revalidations
2244
2238
 
2245
2239
 
2246
- fetchLoadMatches.set(key, [path, match, matches]);
2240
+ fetchLoadMatches.set(key, {
2241
+ routeId,
2242
+ path,
2243
+ match,
2244
+ matches
2245
+ });
2247
2246
  handleFetcherLoader(key, routeId, path, match, matches, submission);
2248
2247
  } // Call the action for the matched fetcher.submit(), and then handle redirects,
2249
2248
  // errors, and revalidation
@@ -2349,11 +2348,8 @@ function createRouter(init) {
2349
2348
  // current fetcher which we want to keep in it's current loading state which
2350
2349
  // contains it's action submission info + action data
2351
2350
 
2352
- revalidatingFetchers.filter(_ref5 => {
2353
- let [staleKey] = _ref5;
2354
- return staleKey !== key;
2355
- }).forEach(_ref6 => {
2356
- let [staleKey] = _ref6;
2351
+ revalidatingFetchers.filter(rf => rf.key !== key).forEach(rf => {
2352
+ let staleKey = rf.key;
2357
2353
  let existingFetcher = state.fetchers.get(staleKey);
2358
2354
  let revalidatingFetcher = {
2359
2355
  state: "loading",
@@ -2382,10 +2378,7 @@ function createRouter(init) {
2382
2378
 
2383
2379
  fetchReloadIds.delete(key);
2384
2380
  fetchControllers.delete(key);
2385
- revalidatingFetchers.forEach(_ref7 => {
2386
- let [staleKey] = _ref7;
2387
- return fetchControllers.delete(staleKey);
2388
- });
2381
+ revalidatingFetchers.forEach(r => fetchControllers.delete(r.key));
2389
2382
  let redirect = findRedirect(results);
2390
2383
 
2391
2384
  if (redirect) {
@@ -2625,16 +2618,10 @@ function createRouter(init) {
2625
2618
  // Call all navigation loaders and revalidating fetcher loaders in parallel,
2626
2619
  // then slice off the results into separate arrays so we can handle them
2627
2620
  // accordingly
2628
- let results = await Promise.all([...matchesToLoad.map(match => callLoaderOrAction("loader", request, match, matches, router.basename)), ...fetchersToLoad.map(_ref8 => {
2629
- let [, href, match, fetchMatches] = _ref8;
2630
- return callLoaderOrAction("loader", createClientSideRequest(init.history, href, request.signal), match, fetchMatches, router.basename);
2631
- })]);
2621
+ let results = await Promise.all([...matchesToLoad.map(match => callLoaderOrAction("loader", request, match, matches, router.basename)), ...fetchersToLoad.map(f => callLoaderOrAction("loader", createClientSideRequest(init.history, f.path, request.signal), f.match, f.matches, router.basename))]);
2632
2622
  let loaderResults = results.slice(0, matchesToLoad.length);
2633
2623
  let fetcherResults = results.slice(matchesToLoad.length);
2634
- await Promise.all([resolveDeferredResults(currentMatches, matchesToLoad, loaderResults, request.signal, false, state.loaderData), resolveDeferredResults(currentMatches, fetchersToLoad.map(_ref9 => {
2635
- let [,, match] = _ref9;
2636
- return match;
2637
- }), fetcherResults, request.signal, true)]);
2624
+ await Promise.all([resolveDeferredResults(currentMatches, matchesToLoad, loaderResults, request.signal, false, state.loaderData), resolveDeferredResults(currentMatches, fetchersToLoad.map(f => f.match), fetcherResults, request.signal, true)]);
2638
2625
  return {
2639
2626
  results,
2640
2627
  loaderResults,
@@ -2773,12 +2760,12 @@ function createRouter(init) {
2773
2760
  });
2774
2761
  }
2775
2762
 
2776
- function shouldBlockNavigation(_ref10) {
2763
+ function shouldBlockNavigation(_ref2) {
2777
2764
  let {
2778
2765
  currentLocation,
2779
2766
  nextLocation,
2780
2767
  historyAction
2781
- } = _ref10;
2768
+ } = _ref2;
2782
2769
 
2783
2770
  if (activeBlocker == null) {
2784
2771
  return;
@@ -3372,24 +3359,15 @@ function normalizeNavigateOptions(to, opts, isFetcher) {
3372
3359
 
3373
3360
 
3374
3361
  let parsedPath = parsePath(path);
3362
+ let searchParams = convertFormDataToSearchParams(opts.formData); // Since fetcher GET submissions only run a single loader (as opposed to
3363
+ // navigation GET submissions which run all loaders), we need to preserve
3364
+ // any incoming ?index params
3375
3365
 
3376
- try {
3377
- let searchParams = convertFormDataToSearchParams(opts.formData); // Since fetcher GET submissions only run a single loader (as opposed to
3378
- // navigation GET submissions which run all loaders), we need to preserve
3379
- // any incoming ?index params
3380
-
3381
- if (isFetcher && parsedPath.search && hasNakedIndexQuery(parsedPath.search)) {
3382
- searchParams.append("index", "");
3383
- }
3384
-
3385
- parsedPath.search = "?" + searchParams;
3386
- } catch (e) {
3387
- return {
3388
- path,
3389
- error: getInternalRouterError(400)
3390
- };
3366
+ if (isFetcher && parsedPath.search && hasNakedIndexQuery(parsedPath.search)) {
3367
+ searchParams.append("index", "");
3391
3368
  }
3392
3369
 
3370
+ parsedPath.search = "?" + searchParams;
3393
3371
  return {
3394
3372
  path: createPath(parsedPath),
3395
3373
  submission
@@ -3413,25 +3391,73 @@ function getLoaderMatchesUntilBoundary(matches, boundaryId) {
3413
3391
  }
3414
3392
 
3415
3393
  function getMatchesToLoad(history, state, matches, submission, location, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, pendingActionData, pendingError, fetchLoadMatches) {
3416
- let actionResult = pendingError ? Object.values(pendingError)[0] : pendingActionData ? Object.values(pendingActionData)[0] : undefined; // Pick navigation matches that are net-new or qualify for revalidation
3394
+ let actionResult = pendingError ? Object.values(pendingError)[0] : pendingActionData ? Object.values(pendingActionData)[0] : undefined;
3395
+ let currentUrl = history.createURL(state.location);
3396
+ let nextUrl = history.createURL(location);
3397
+ let defaultShouldRevalidate = // Forced revalidation due to submission, useRevalidate, or X-Remix-Revalidate
3398
+ isRevalidationRequired || // Clicked the same link, resubmitted a GET form
3399
+ currentUrl.toString() === nextUrl.toString() || // Search params affect all loaders
3400
+ currentUrl.search !== nextUrl.search; // Pick navigation matches that are net-new or qualify for revalidation
3417
3401
 
3418
3402
  let boundaryId = pendingError ? Object.keys(pendingError)[0] : undefined;
3419
3403
  let boundaryMatches = getLoaderMatchesUntilBoundary(matches, boundaryId);
3420
- let navigationMatches = boundaryMatches.filter((match, index) => match.route.loader != null && (isNewLoader(state.loaderData, state.matches[index], match) || // If this route had a pending deferred cancelled it must be revalidated
3421
- cancelledDeferredRoutes.some(id => id === match.route.id) || shouldRevalidateLoader(history, state.location, state.matches[index], submission, location, match, isRevalidationRequired, actionResult))); // Pick fetcher.loads that need to be revalidated
3404
+ let navigationMatches = boundaryMatches.filter((match, index) => {
3405
+ if (match.route.loader == null) {
3406
+ return false;
3407
+ } // Always call the loader on new route instances and pending defer cancellations
3422
3408
 
3423
- let revalidatingFetchers = [];
3424
- fetchLoadMatches && fetchLoadMatches.forEach((_ref11, key) => {
3425
- let [href, match, fetchMatches] = _ref11;
3426
3409
 
3427
- // This fetcher was cancelled from a prior action submission - force reload
3428
- if (cancelledFetcherLoads.includes(key)) {
3429
- revalidatingFetchers.push([key, href, match, fetchMatches]);
3430
- } else if (isRevalidationRequired) {
3431
- let shouldRevalidate = shouldRevalidateLoader(history, href, match, submission, href, match, isRevalidationRequired, actionResult);
3410
+ if (isNewLoader(state.loaderData, state.matches[index], match) || cancelledDeferredRoutes.some(id => id === match.route.id)) {
3411
+ return true;
3412
+ } // This is the default implementation for when we revalidate. If the route
3413
+ // provides it's own implementation, then we give them full control but
3414
+ // provide this value so they can leverage it if needed after they check
3415
+ // their own specific use cases
3416
+
3417
+
3418
+ let currentRouteMatch = state.matches[index];
3419
+ let nextRouteMatch = match;
3420
+ return shouldRevalidateLoader(match, _extends({
3421
+ currentUrl,
3422
+ currentParams: currentRouteMatch.params,
3423
+ nextUrl,
3424
+ nextParams: nextRouteMatch.params
3425
+ }, submission, {
3426
+ actionResult,
3427
+ defaultShouldRevalidate: defaultShouldRevalidate || isNewRouteInstance(currentRouteMatch, nextRouteMatch)
3428
+ }));
3429
+ }); // Pick fetcher.loads that need to be revalidated
3430
+
3431
+ let revalidatingFetchers = [];
3432
+ fetchLoadMatches && fetchLoadMatches.forEach((f, key) => {
3433
+ if (!matches.some(m => m.route.id === f.routeId)) {
3434
+ // This fetcher is not going to be present in the subsequent render so
3435
+ // there's no need to revalidate it
3436
+ return;
3437
+ } else if (cancelledFetcherLoads.includes(key)) {
3438
+ // This fetcher was cancelled from a prior action submission - force reload
3439
+ revalidatingFetchers.push(_extends({
3440
+ key
3441
+ }, f));
3442
+ } else {
3443
+ // Revalidating fetchers are decoupled from the route matches since they
3444
+ // hit a static href, so they _always_ check shouldRevalidate and the
3445
+ // default is strictly if a revalidation is explicitly required (action
3446
+ // submissions, useRevalidator, X-Remix-Revalidate).
3447
+ let shouldRevalidate = shouldRevalidateLoader(f.match, _extends({
3448
+ currentUrl,
3449
+ currentParams: state.matches[state.matches.length - 1].params,
3450
+ nextUrl,
3451
+ nextParams: matches[matches.length - 1].params
3452
+ }, submission, {
3453
+ actionResult,
3454
+ defaultShouldRevalidate
3455
+ }));
3432
3456
 
3433
3457
  if (shouldRevalidate) {
3434
- revalidatingFetchers.push([key, href, match, fetchMatches]);
3458
+ revalidatingFetchers.push(_extends({
3459
+ key
3460
+ }, f));
3435
3461
  }
3436
3462
  }
3437
3463
  });
@@ -3454,43 +3480,20 @@ function isNewRouteInstance(currentMatch, match) {
3454
3480
  return (// param change for this match, /users/123 -> /users/456
3455
3481
  currentMatch.pathname !== match.pathname || // splat param changed, which is not present in match.path
3456
3482
  // e.g. /files/images/avatar.jpg -> files/finances.xls
3457
- currentPath && currentPath.endsWith("*") && currentMatch.params["*"] !== match.params["*"]
3483
+ currentPath != null && currentPath.endsWith("*") && currentMatch.params["*"] !== match.params["*"]
3458
3484
  );
3459
3485
  }
3460
3486
 
3461
- function shouldRevalidateLoader(history, currentLocation, currentMatch, submission, location, match, isRevalidationRequired, actionResult) {
3462
- let currentUrl = history.createURL(currentLocation);
3463
- let currentParams = currentMatch.params;
3464
- let nextUrl = history.createURL(location);
3465
- let nextParams = match.params; // This is the default implementation as to when we revalidate. If the route
3466
- // provides it's own implementation, then we give them full control but
3467
- // provide this value so they can leverage it if needed after they check
3468
- // their own specific use cases
3469
- // Note that fetchers always provide the same current/next locations so the
3470
- // URL-based checks here don't apply to fetcher shouldRevalidate calls
3471
-
3472
- let defaultShouldRevalidate = isNewRouteInstance(currentMatch, match) || // Clicked the same link, resubmitted a GET form
3473
- currentUrl.toString() === nextUrl.toString() || // Search params affect all loaders
3474
- currentUrl.search !== nextUrl.search || // Forced revalidation due to submission, useRevalidate, or X-Remix-Revalidate
3475
- isRevalidationRequired;
3476
-
3477
- if (match.route.shouldRevalidate) {
3478
- let routeChoice = match.route.shouldRevalidate(_extends({
3479
- currentUrl,
3480
- currentParams,
3481
- nextUrl,
3482
- nextParams
3483
- }, submission, {
3484
- actionResult,
3485
- defaultShouldRevalidate
3486
- }));
3487
+ function shouldRevalidateLoader(loaderMatch, arg) {
3488
+ if (loaderMatch.route.shouldRevalidate) {
3489
+ let routeChoice = loaderMatch.route.shouldRevalidate(arg);
3487
3490
 
3488
3491
  if (typeof routeChoice === "boolean") {
3489
3492
  return routeChoice;
3490
3493
  }
3491
3494
  }
3492
3495
 
3493
- return defaultShouldRevalidate;
3496
+ return arg.defaultShouldRevalidate;
3494
3497
  }
3495
3498
 
3496
3499
  async function callLoaderOrAction(type, request, match, matches, basename, isStaticRequest, isRouteRequest, requestContext) {
@@ -3665,8 +3668,8 @@ function convertFormDataToSearchParams(formData) {
3665
3668
  let searchParams = new URLSearchParams();
3666
3669
 
3667
3670
  for (let [key, value] of formData.entries()) {
3668
- invariant(typeof value === "string", 'File inputs are not supported with encType "application/x-www-form-urlencoded", ' + 'please use "multipart/form-data" instead.');
3669
- searchParams.append(key, value);
3671
+ // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#converting-an-entry-list-to-a-list-of-name-value-pairs
3672
+ searchParams.append(key, value instanceof File ? value.name : value);
3670
3673
  }
3671
3674
 
3672
3675
  return searchParams;
@@ -3757,7 +3760,10 @@ function processLoaderData(state, matches, matchesToLoad, results, pendingError,
3757
3760
  } = processRouteLoaderData(matches, matchesToLoad, results, pendingError, activeDeferreds); // Process results from our revalidating fetchers
3758
3761
 
3759
3762
  for (let index = 0; index < revalidatingFetchers.length; index++) {
3760
- let [key,, match] = revalidatingFetchers[index];
3763
+ let {
3764
+ key,
3765
+ match
3766
+ } = revalidatingFetchers[index];
3761
3767
  invariant(fetcherResults !== undefined && fetcherResults[index] !== undefined, "Did not find corresponding fetcher result");
3762
3768
  let result = fetcherResults[index]; // Process fetcher non-redirect errors
3763
3769
 
@@ -3863,8 +3869,6 @@ function getInternalRouterError(status, _temp4) {
3863
3869
  errorMessage = "You made a " + method + " request to \"" + pathname + "\" but " + ("did not provide a `loader` for route \"" + routeId + "\", ") + "so there is no way to handle the request.";
3864
3870
  } else if (type === "defer-action") {
3865
3871
  errorMessage = "defer() is not supported in actions";
3866
- } else {
3867
- errorMessage = "Cannot submit binary form data using GET";
3868
3872
  }
3869
3873
  } else if (status === 403) {
3870
3874
  statusText = "Forbidden";