Spaces:
Running
Running
| ; | |
| Object.defineProperty(exports, "__esModule", { | |
| value: true | |
| }); | |
| 0 && (module.exports = { | |
| handleAction: null, | |
| parseHostHeader: null | |
| }); | |
| function _export(target, all) { | |
| for(var name in all)Object.defineProperty(target, name, { | |
| enumerable: true, | |
| get: all[name] | |
| }); | |
| } | |
| _export(exports, { | |
| handleAction: function() { | |
| return handleAction; | |
| }, | |
| parseHostHeader: function() { | |
| return parseHostHeader; | |
| } | |
| }); | |
| const _approuterheaders = require("../../client/components/app-router-headers"); | |
| const _httpaccessfallback = require("../../client/components/http-access-fallback/http-access-fallback"); | |
| const _redirect = require("../../client/components/redirect"); | |
| const _redirecterror = require("../../client/components/redirect-error"); | |
| const _renderresult = /*#__PURE__*/ _interop_require_default(require("../render-result")); | |
| const _flightrenderresult = require("./flight-render-result"); | |
| const _utils = require("../lib/server-ipc/utils"); | |
| const _requestcookies = require("../web/spec-extension/adapters/request-cookies"); | |
| const _constants = require("../../lib/constants"); | |
| const _serveractionrequestmeta = require("../lib/server-action-request-meta"); | |
| const _csrfprotection = require("./csrf-protection"); | |
| const _log = require("../../build/output/log"); | |
| const _cookies = require("../web/spec-extension/cookies"); | |
| const _headers = require("../web/spec-extension/adapters/headers"); | |
| const _utils1 = require("../web/utils"); | |
| const _manifestssingleton = require("./manifests-singleton"); | |
| const _helpers = require("../base-http/helpers"); | |
| const _redirectstatuscode = require("../../client/components/redirect-status-code"); | |
| const _requeststore = require("../async-storage/request-store"); | |
| const _workunitasyncstorageexternal = require("../app-render/work-unit-async-storage.external"); | |
| const _invarianterror = require("../../shared/lib/invariant-error"); | |
| const _revalidationutils = require("../revalidation-utils"); | |
| const _requestmeta = require("../request-meta"); | |
| const _setcachebustingsearchparam = require("../../client/components/router-reducer/set-cache-busting-search-param"); | |
| const _actionrevalidationkind = require("../../shared/lib/action-revalidation-kind"); | |
| function _interop_require_default(obj) { | |
| return obj && obj.__esModule ? obj : { | |
| default: obj | |
| }; | |
| } | |
| /** | |
| * Checks if the app has any server actions defined in any runtime. | |
| */ function hasServerActions() { | |
| const serverActionsManifest = (0, _manifestssingleton.getServerActionsManifest)(); | |
| return Object.keys(serverActionsManifest.node).length > 0 || Object.keys(serverActionsManifest.edge).length > 0; | |
| } | |
| function nodeHeadersToRecord(headers) { | |
| const record = {}; | |
| for (const [key, value] of Object.entries(headers)){ | |
| if (value !== undefined) { | |
| record[key] = Array.isArray(value) ? value.join(', ') : `${value}`; | |
| } | |
| } | |
| return record; | |
| } | |
| function getForwardedHeaders(req, res) { | |
| // Get request headers and cookies | |
| const requestHeaders = req.headers; | |
| const requestCookies = new _cookies.RequestCookies(_headers.HeadersAdapter.from(requestHeaders)); | |
| // Get response headers and cookies | |
| const responseHeaders = res.getHeaders(); | |
| const responseCookies = new _cookies.ResponseCookies((0, _utils1.fromNodeOutgoingHttpHeaders)(responseHeaders)); | |
| // Merge request and response headers | |
| const mergedHeaders = (0, _utils.filterReqHeaders)({ | |
| ...nodeHeadersToRecord(requestHeaders), | |
| ...nodeHeadersToRecord(responseHeaders) | |
| }, _utils.actionsForbiddenHeaders); | |
| // Merge cookies into requestCookies, so responseCookies always take precedence | |
| // and overwrite/delete those from requestCookies. | |
| responseCookies.getAll().forEach((cookie)=>{ | |
| if (typeof cookie.value === 'undefined') { | |
| requestCookies.delete(cookie.name); | |
| } else { | |
| requestCookies.set(cookie); | |
| } | |
| }); | |
| // Update the 'cookie' header with the merged cookies | |
| mergedHeaders['cookie'] = requestCookies.toString(); | |
| // Remove headers that should not be forwarded | |
| delete mergedHeaders['transfer-encoding']; | |
| return new Headers(mergedHeaders); | |
| } | |
| function addRevalidationHeader(res, { workStore, requestStore }) { | |
| var _workStore_pendingRevalidatedTags; | |
| // If a tag was revalidated, the client router needs to invalidate all the | |
| // client router cache as they may be stale. And if a path was revalidated, the | |
| // client needs to invalidate all subtrees below that path. | |
| // TODO: Currently we don't send the specific tags or paths to the client, | |
| // we just send a flag indicating that all the static data on the client | |
| // should be invalidated. In the future, this will likely be a Bloom filter | |
| // or bitmask of some kind. | |
| // TODO-APP: Currently the prefetch cache doesn't have subtree information, | |
| // so we need to invalidate the entire cache if a path was revalidated. | |
| // TODO-APP: Currently paths are treated as tags, so the second element of the tuple | |
| // is always empty. | |
| const isTagRevalidated = ((_workStore_pendingRevalidatedTags = workStore.pendingRevalidatedTags) == null ? void 0 : _workStore_pendingRevalidatedTags.length) ? 1 : 0; | |
| const isCookieRevalidated = (0, _requestcookies.getModifiedCookieValues)(requestStore.mutableCookies).length ? 1 : 0; | |
| // First check if a tag, cookie, or path was revalidated. | |
| if (isTagRevalidated || isCookieRevalidated) { | |
| res.setHeader(_approuterheaders.NEXT_ACTION_REVALIDATED_HEADER, JSON.stringify(_actionrevalidationkind.ActionDidRevalidateStaticAndDynamic)); | |
| } else if (// Check for refresh() actions. This will invalidate only the dynamic data. | |
| workStore.pathWasRevalidated !== undefined && workStore.pathWasRevalidated !== _actionrevalidationkind.ActionDidNotRevalidate) { | |
| res.setHeader(_approuterheaders.NEXT_ACTION_REVALIDATED_HEADER, JSON.stringify(workStore.pathWasRevalidated)); | |
| } | |
| } | |
| /** | |
| * Forwards a server action request to a separate worker. Used when the requested action is not available in the current worker. | |
| */ async function createForwardedActionResponse(req, res, host, workerPathname, basePath) { | |
| var _getRequestMeta; | |
| if (!host) { | |
| throw Object.defineProperty(new Error('Invariant: Missing `host` header from a forwarded Server Actions request.'), "__NEXT_ERROR_CODE", { | |
| value: "E226", | |
| enumerable: false, | |
| configurable: true | |
| }); | |
| } | |
| const forwardedHeaders = getForwardedHeaders(req, res); | |
| // indicate that this action request was forwarded from another worker | |
| // we use this to skip rendering the flight tree so that we don't update the UI | |
| // with the response from the forwarded worker | |
| forwardedHeaders.set('x-action-forwarded', '1'); | |
| const proto = ((_getRequestMeta = (0, _requestmeta.getRequestMeta)(req, 'initProtocol')) == null ? void 0 : _getRequestMeta.replace(/:+$/, '')) || 'https'; | |
| // For standalone or the serverful mode, use the internal origin directly | |
| // other than the host headers from the request. | |
| const origin = process.env.__NEXT_PRIVATE_ORIGIN || `${proto}://${host.value}`; | |
| const fetchUrl = new URL(`${origin}${basePath}${workerPathname}`); | |
| try { | |
| var _response_headers_get; | |
| let body; | |
| if (// The type check here ensures that `req` is correctly typed, and the | |
| // environment variable check provides dead code elimination. | |
| process.env.NEXT_RUNTIME === 'edge' && (0, _helpers.isWebNextRequest)(req)) { | |
| if (!req.body) { | |
| throw Object.defineProperty(new Error('Invariant: missing request body.'), "__NEXT_ERROR_CODE", { | |
| value: "E333", | |
| enumerable: false, | |
| configurable: true | |
| }); | |
| } | |
| body = req.body; | |
| } else if (// The type check here ensures that `req` is correctly typed, and the | |
| // environment variable check provides dead code elimination. | |
| process.env.NEXT_RUNTIME !== 'edge' && (0, _helpers.isNodeNextRequest)(req)) { | |
| body = req.stream(); | |
| } else { | |
| throw Object.defineProperty(new Error('Invariant: Unknown request type.'), "__NEXT_ERROR_CODE", { | |
| value: "E114", | |
| enumerable: false, | |
| configurable: true | |
| }); | |
| } | |
| // Forward the request to the new worker | |
| const response = await fetch(fetchUrl, { | |
| method: 'POST', | |
| body, | |
| duplex: 'half', | |
| headers: forwardedHeaders, | |
| redirect: 'manual', | |
| next: { | |
| // @ts-ignore | |
| internal: 1 | |
| } | |
| }); | |
| if ((_response_headers_get = response.headers.get('content-type')) == null ? void 0 : _response_headers_get.startsWith(_approuterheaders.RSC_CONTENT_TYPE_HEADER)) { | |
| // copy the headers from the redirect response to the response we're sending | |
| for (const [key, value] of response.headers){ | |
| if (!_utils.actionsForbiddenHeaders.includes(key)) { | |
| res.setHeader(key, value); | |
| } | |
| } | |
| return new _flightrenderresult.FlightRenderResult(response.body); | |
| } else { | |
| var // Since we aren't consuming the response body, we cancel it to avoid memory leaks | |
| _response_body; | |
| (_response_body = response.body) == null ? void 0 : _response_body.cancel(); | |
| } | |
| } catch (err) { | |
| // we couldn't stream the forwarded response, so we'll just return an empty response | |
| console.error(`failed to forward action response`, err); | |
| } | |
| return _renderresult.default.fromStatic('{}', _constants.JSON_CONTENT_TYPE_HEADER); | |
| } | |
| /** | |
| * Returns the parsed redirect URL if we deem that it is hosted by us. | |
| * | |
| * We handle both relative and absolute redirect URLs. | |
| * | |
| * In case the redirect URL is not relative to the application we return `null`. | |
| */ function getAppRelativeRedirectUrl(basePath, host, redirectUrl, currentPathname) { | |
| if (redirectUrl.startsWith('/')) { | |
| // Absolute path - just add basePath | |
| return new URL(`${basePath}${redirectUrl}`, 'http://n'); | |
| } else if (redirectUrl.startsWith('.')) { | |
| // Relative path - resolve relative to current pathname | |
| let base = currentPathname || '/'; | |
| // Ensure the base path ends with a slash so relative resolution works correctly | |
| // e.g., "./subpage" from "/subdir" should resolve to "/subdir/subpage" | |
| // not "/subpage" | |
| if (!base.endsWith('/')) { | |
| base = base + '/'; | |
| } | |
| const resolved = new URL(redirectUrl, `http://n${base}`); | |
| // Include basePath in the final URL | |
| return new URL(`${basePath}${resolved.pathname}${resolved.search}${resolved.hash}`, 'http://n'); | |
| } | |
| const parsedRedirectUrl = new URL(redirectUrl); | |
| if ((host == null ? void 0 : host.value) !== parsedRedirectUrl.host) { | |
| return null; | |
| } | |
| // At this point the hosts are the same, just confirm we | |
| // are routing to a path underneath the `basePath` | |
| return parsedRedirectUrl.pathname.startsWith(basePath) ? parsedRedirectUrl : null; | |
| } | |
| async function createRedirectRenderResult(req, res, originalHost, redirectUrl, redirectType, basePath, workStore, currentPathname) { | |
| res.setHeader('x-action-redirect', `${redirectUrl};${redirectType}`); | |
| // If we're redirecting to another route of this Next.js application, we'll | |
| // try to stream the response from the other worker path. When that works, | |
| // we can save an extra roundtrip and avoid a full page reload. | |
| // When the redirect URL starts with a `/` or is to the same host, under the | |
| // `basePath` we treat it as an app-relative redirect; | |
| const appRelativeRedirectUrl = getAppRelativeRedirectUrl(basePath, originalHost, redirectUrl, currentPathname); | |
| if (appRelativeRedirectUrl) { | |
| var _getRequestMeta; | |
| if (!originalHost) { | |
| throw Object.defineProperty(new Error('Invariant: Missing `host` header from a forwarded Server Actions request.'), "__NEXT_ERROR_CODE", { | |
| value: "E226", | |
| enumerable: false, | |
| configurable: true | |
| }); | |
| } | |
| const forwardedHeaders = getForwardedHeaders(req, res); | |
| forwardedHeaders.set(_approuterheaders.RSC_HEADER, '1'); | |
| const proto = ((_getRequestMeta = (0, _requestmeta.getRequestMeta)(req, 'initProtocol')) == null ? void 0 : _getRequestMeta.replace(/:+$/, '')) || 'https'; | |
| // For standalone or the serverful mode, use the internal origin directly | |
| // other than the host headers from the request. | |
| const origin = process.env.__NEXT_PRIVATE_ORIGIN || `${proto}://${originalHost.value}`; | |
| const fetchUrl = new URL(`${origin}${appRelativeRedirectUrl.pathname}${appRelativeRedirectUrl.search}`); | |
| if (workStore.pendingRevalidatedTags) { | |
| var _workStore_incrementalCache_prerenderManifest_preview, _workStore_incrementalCache_prerenderManifest, _workStore_incrementalCache; | |
| forwardedHeaders.set(_constants.NEXT_CACHE_REVALIDATED_TAGS_HEADER, workStore.pendingRevalidatedTags.map((item)=>item.tag).join(',')); | |
| forwardedHeaders.set(_constants.NEXT_CACHE_REVALIDATE_TAG_TOKEN_HEADER, ((_workStore_incrementalCache = workStore.incrementalCache) == null ? void 0 : (_workStore_incrementalCache_prerenderManifest = _workStore_incrementalCache.prerenderManifest) == null ? void 0 : (_workStore_incrementalCache_prerenderManifest_preview = _workStore_incrementalCache_prerenderManifest.preview) == null ? void 0 : _workStore_incrementalCache_prerenderManifest_preview.previewModeId) || ''); | |
| } | |
| // Ensures that when the path was revalidated we don't return a partial response on redirects | |
| forwardedHeaders.delete(_approuterheaders.NEXT_ROUTER_STATE_TREE_HEADER); | |
| // When an action follows a redirect, it's no longer handling an action: it's just a normal RSC request | |
| // to the requested URL. We should remove the `next-action` header so that it's not treated as an action | |
| forwardedHeaders.delete(_approuterheaders.ACTION_HEADER); | |
| try { | |
| var _response_headers_get; | |
| (0, _setcachebustingsearchparam.setCacheBustingSearchParam)(fetchUrl, { | |
| [_approuterheaders.NEXT_ROUTER_PREFETCH_HEADER]: forwardedHeaders.get(_approuterheaders.NEXT_ROUTER_PREFETCH_HEADER) ? '1' : undefined, | |
| [_approuterheaders.NEXT_ROUTER_SEGMENT_PREFETCH_HEADER]: forwardedHeaders.get(_approuterheaders.NEXT_ROUTER_SEGMENT_PREFETCH_HEADER) ?? undefined, | |
| [_approuterheaders.NEXT_ROUTER_STATE_TREE_HEADER]: forwardedHeaders.get(_approuterheaders.NEXT_ROUTER_STATE_TREE_HEADER) ?? undefined, | |
| [_approuterheaders.NEXT_URL]: forwardedHeaders.get(_approuterheaders.NEXT_URL) ?? undefined | |
| }); | |
| const response = await fetch(fetchUrl, { | |
| method: 'GET', | |
| headers: forwardedHeaders, | |
| next: { | |
| // @ts-ignore | |
| internal: 1 | |
| } | |
| }); | |
| if ((_response_headers_get = response.headers.get('content-type')) == null ? void 0 : _response_headers_get.startsWith(_approuterheaders.RSC_CONTENT_TYPE_HEADER)) { | |
| // copy the headers from the redirect response to the response we're sending | |
| for (const [key, value] of response.headers){ | |
| if (!_utils.actionsForbiddenHeaders.includes(key)) { | |
| res.setHeader(key, value); | |
| } | |
| } | |
| return new _flightrenderresult.FlightRenderResult(response.body); | |
| } else { | |
| var // Since we aren't consuming the response body, we cancel it to avoid memory leaks | |
| _response_body; | |
| (_response_body = response.body) == null ? void 0 : _response_body.cancel(); | |
| } | |
| } catch (err) { | |
| // we couldn't stream the redirect response, so we'll just do a normal redirect | |
| console.error(`failed to get redirect response`, err); | |
| } | |
| } | |
| return _renderresult.default.EMPTY; | |
| } | |
| /** | |
| * Ensures the value of the header can't create long logs. | |
| */ function limitUntrustedHeaderValueForLogs(value) { | |
| return value.length > 100 ? value.slice(0, 100) + '...' : value; | |
| } | |
| function parseHostHeader(headers, originDomain) { | |
| var _forwardedHostHeader_split_, _forwardedHostHeader_split; | |
| const forwardedHostHeader = headers['x-forwarded-host']; | |
| const forwardedHostHeaderValue = forwardedHostHeader && Array.isArray(forwardedHostHeader) ? forwardedHostHeader[0] : forwardedHostHeader == null ? void 0 : (_forwardedHostHeader_split = forwardedHostHeader.split(',')) == null ? void 0 : (_forwardedHostHeader_split_ = _forwardedHostHeader_split[0]) == null ? void 0 : _forwardedHostHeader_split_.trim(); | |
| const hostHeader = headers['host']; | |
| if (originDomain) { | |
| return forwardedHostHeaderValue === originDomain ? { | |
| type: "x-forwarded-host", | |
| value: forwardedHostHeaderValue | |
| } : hostHeader === originDomain ? { | |
| type: "host", | |
| value: hostHeader | |
| } : undefined; | |
| } | |
| return forwardedHostHeaderValue ? { | |
| type: "x-forwarded-host", | |
| value: forwardedHostHeaderValue | |
| } : hostHeader ? { | |
| type: "host", | |
| value: hostHeader | |
| } : undefined; | |
| } | |
| async function handleAction({ req, res, ComponentMod, generateFlight, workStore, requestStore, serverActions, ctx, metadata }) { | |
| const contentType = req.headers['content-type']; | |
| const { page } = ctx.renderOpts; | |
| const serverModuleMap = (0, _manifestssingleton.getServerModuleMap)(); | |
| const { actionId, isMultipartAction, isFetchAction, isURLEncodedAction, isPossibleServerAction } = (0, _serveractionrequestmeta.getServerActionRequestMetadata)(req); | |
| const handleUnrecognizedFetchAction = (err)=>{ | |
| // If the deployment doesn't have skew protection, this is expected to occasionally happen, | |
| // so we use a warning instead of an error. | |
| console.warn(err); | |
| // Return an empty response with a header that the client router will interpret. | |
| // We don't need to waste time encoding a flight response, and using a blank body + header | |
| // means that unrecognized actions can also be handled at the infra level | |
| // (i.e. without needing to invoke a lambda) | |
| res.setHeader(_approuterheaders.NEXT_ACTION_NOT_FOUND_HEADER, '1'); | |
| res.setHeader('content-type', 'text/plain'); | |
| res.statusCode = 404; | |
| return { | |
| type: 'done', | |
| result: _renderresult.default.fromStatic('Server action not found.', 'text/plain') | |
| }; | |
| }; | |
| // If it can't be a Server Action, skip handling. | |
| // Note that this can be a false positive -- any multipart/urlencoded POST can get us here, | |
| // But won't know if it's an MPA action or not until we call `decodeAction` below. | |
| if (!isPossibleServerAction) { | |
| return null; | |
| } | |
| // We don't currently support URL encoded actions, so we bail out early. | |
| // Depending on if it's a fetch action or an MPA, we return a different response. | |
| if (isURLEncodedAction) { | |
| if (isFetchAction) { | |
| return { | |
| type: 'not-found' | |
| }; | |
| } else { | |
| // This is an MPA action, so we return null | |
| return null; | |
| } | |
| } | |
| // If the app has no server actions at all, we can 404 early. | |
| if (!hasServerActions()) { | |
| return handleUnrecognizedFetchAction(getActionNotFoundError(actionId)); | |
| } | |
| if (workStore.isStaticGeneration) { | |
| throw Object.defineProperty(new Error("Invariant: server actions can't be handled during static rendering"), "__NEXT_ERROR_CODE", { | |
| value: "E359", | |
| enumerable: false, | |
| configurable: true | |
| }); | |
| } | |
| let temporaryReferences; | |
| // When running actions the default is no-store, you can still `cache: 'force-cache'` | |
| workStore.fetchCache = 'default-no-store'; | |
| const originHeader = req.headers['origin']; | |
| const originDomain = typeof originHeader === 'string' && originHeader !== 'null' ? new URL(originHeader).host : undefined; | |
| const host = parseHostHeader(req.headers); | |
| let warning = undefined; | |
| function warnBadServerActionRequest() { | |
| if (warning) { | |
| (0, _log.warn)(warning); | |
| } | |
| } | |
| // This is to prevent CSRF attacks. If `x-forwarded-host` is set, we need to | |
| // ensure that the request is coming from the same host. | |
| if (!originDomain) { | |
| // This might be an old browser that doesn't send `host` header. We ignore | |
| // this case. | |
| warning = 'Missing `origin` header from a forwarded Server Actions request.'; | |
| } else if (!host || originDomain !== host.value) { | |
| // If the customer sets a list of allowed origins, we'll allow the request. | |
| // These are considered safe but might be different from forwarded host set | |
| // by the infra (i.e. reverse proxies). | |
| if ((0, _csrfprotection.isCsrfOriginAllowed)(originDomain, serverActions == null ? void 0 : serverActions.allowedOrigins)) { | |
| // Ignore it | |
| } else { | |
| if (host) { | |
| // This seems to be an CSRF attack. We should not proceed the action. | |
| console.error(`\`${host.type}\` header with value \`${limitUntrustedHeaderValueForLogs(host.value)}\` does not match \`origin\` header with value \`${limitUntrustedHeaderValueForLogs(originDomain)}\` from a forwarded Server Actions request. Aborting the action.`); | |
| } else { | |
| // This is an attack. We should not proceed the action. | |
| console.error(`\`x-forwarded-host\` or \`host\` headers are not provided. One of these is needed to compare the \`origin\` header from a forwarded Server Actions request. Aborting the action.`); | |
| } | |
| const error = Object.defineProperty(new Error('Invalid Server Actions request.'), "__NEXT_ERROR_CODE", { | |
| value: "E80", | |
| enumerable: false, | |
| configurable: true | |
| }); | |
| if (isFetchAction) { | |
| res.statusCode = 500; | |
| metadata.statusCode = 500; | |
| const promise = Promise.reject(error); | |
| try { | |
| // we need to await the promise to trigger the rejection early | |
| // so that it's already handled by the time we call | |
| // the RSC runtime. Otherwise, it will throw an unhandled | |
| // promise rejection error in the renderer. | |
| await promise; | |
| } catch { | |
| // swallow error, it's gonna be handled on the client | |
| } | |
| return { | |
| type: 'done', | |
| result: await generateFlight(req, ctx, requestStore, { | |
| actionResult: promise, | |
| // We didn't execute an action, so no revalidations could have | |
| // occurred. We can skip rendering the page. | |
| skipPageRendering: true, | |
| temporaryReferences | |
| }) | |
| }; | |
| } | |
| throw error; | |
| } | |
| } | |
| // ensure we avoid caching server actions unexpectedly | |
| res.setHeader('Cache-Control', 'no-cache, no-store, max-age=0, must-revalidate'); | |
| const { actionAsyncStorage } = ComponentMod; | |
| const actionWasForwarded = Boolean(req.headers['x-action-forwarded']); | |
| if (actionId) { | |
| const forwardedWorker = (0, _manifestssingleton.selectWorkerForForwarding)(actionId, page); | |
| // If forwardedWorker is truthy, it means there isn't a worker for the action | |
| // in the current handler, so we forward the request to a worker that has the action. | |
| if (forwardedWorker) { | |
| return { | |
| type: 'done', | |
| result: await createForwardedActionResponse(req, res, host, forwardedWorker, ctx.renderOpts.basePath) | |
| }; | |
| } | |
| } | |
| try { | |
| return await actionAsyncStorage.run({ | |
| isAction: true | |
| }, async ()=>{ | |
| // We only use these for fetch actions -- MPA actions handle them inside `decodeAction`. | |
| let actionModId; | |
| let boundActionArguments = []; | |
| if (// The type check here ensures that `req` is correctly typed, and the | |
| // environment variable check provides dead code elimination. | |
| process.env.NEXT_RUNTIME === 'edge' && (0, _helpers.isWebNextRequest)(req)) { | |
| if (!req.body) { | |
| throw Object.defineProperty(new Error('invariant: Missing request body.'), "__NEXT_ERROR_CODE", { | |
| value: "E364", | |
| enumerable: false, | |
| configurable: true | |
| }); | |
| } | |
| // TODO: add body limit | |
| // Use react-server-dom-webpack/server | |
| const { createTemporaryReferenceSet, decodeReply, decodeAction, decodeFormState } = ComponentMod; | |
| temporaryReferences = createTemporaryReferenceSet(); | |
| if (isMultipartAction) { | |
| // TODO-APP: Add streaming support | |
| const formData = await req.request.formData(); | |
| if (isFetchAction) { | |
| // A fetch action with a multipart body. | |
| try { | |
| actionModId = getActionModIdOrError(actionId, serverModuleMap); | |
| } catch (err) { | |
| return handleUnrecognizedFetchAction(err); | |
| } | |
| boundActionArguments = await decodeReply(formData, serverModuleMap, { | |
| temporaryReferences | |
| }); | |
| } else { | |
| // Multipart POST, but not a fetch action. | |
| // Potentially an MPA action, we have to try decoding it to check. | |
| if (areAllActionIdsValid(formData, serverModuleMap) === false) { | |
| // TODO: This can be from skew or manipulated input. We should handle this case | |
| // more gracefully but this preserves the prior behavior where decodeAction would throw instead. | |
| throw Object.defineProperty(new Error(`Failed to find Server Action. This request might be from an older or newer deployment.\nRead more: https://nextjs.org/docs/messages/failed-to-find-server-action`), "__NEXT_ERROR_CODE", { | |
| value: "E975", | |
| enumerable: false, | |
| configurable: true | |
| }); | |
| } | |
| const action = await decodeAction(formData, serverModuleMap); | |
| if (typeof action === 'function') { | |
| // an MPA action. | |
| // Only warn if it's a server action, otherwise skip for other post requests | |
| warnBadServerActionRequest(); | |
| const { actionResult } = await executeActionAndPrepareForRender(action, [], workStore, requestStore, actionWasForwarded); | |
| const formState = await decodeFormState(actionResult, formData, serverModuleMap); | |
| // Skip the fetch path. | |
| // We need to render a full HTML version of the page for the response, we'll handle that in app-render. | |
| return { | |
| type: 'done', | |
| result: undefined, | |
| formState | |
| }; | |
| } else { | |
| // We couldn't decode an action, so this POST request turned out not to be a server action request. | |
| return null; | |
| } | |
| } | |
| } else { | |
| // POST with non-multipart body. | |
| // If it's not multipart AND not a fetch action, | |
| // then it can't be an action request. | |
| if (!isFetchAction) { | |
| return null; | |
| } | |
| try { | |
| actionModId = getActionModIdOrError(actionId, serverModuleMap); | |
| } catch (err) { | |
| return handleUnrecognizedFetchAction(err); | |
| } | |
| // A fetch action with a non-multipart body. | |
| // In practice, this happens if `encodeReply` returned a string instead of FormData, | |
| // which can happen for very simple JSON-like values that don't need multiple flight rows. | |
| const chunks = []; | |
| const reader = req.body.getReader(); | |
| while(true){ | |
| const { done, value } = await reader.read(); | |
| if (done) { | |
| break; | |
| } | |
| chunks.push(value); | |
| } | |
| const actionData = Buffer.concat(chunks).toString('utf-8'); | |
| boundActionArguments = await decodeReply(actionData, serverModuleMap, { | |
| temporaryReferences | |
| }); | |
| } | |
| } else if (// The type check here ensures that `req` is correctly typed, and the | |
| // environment variable check provides dead code elimination. | |
| process.env.NEXT_RUNTIME !== 'edge' && (0, _helpers.isNodeNextRequest)(req)) { | |
| // Use react-server-dom-webpack/server.node which supports streaming | |
| const { createTemporaryReferenceSet, decodeReply, decodeReplyFromBusboy, decodeAction, decodeFormState } = require(`./react-server.node`); | |
| temporaryReferences = createTemporaryReferenceSet(); | |
| const { PassThrough, Readable, Transform } = require('node:stream'); | |
| const { pipeline } = require('node:stream/promises'); | |
| const defaultBodySizeLimit = '1 MB'; | |
| const bodySizeLimit = (serverActions == null ? void 0 : serverActions.bodySizeLimit) ?? defaultBodySizeLimit; | |
| const bodySizeLimitBytes = bodySizeLimit !== defaultBodySizeLimit ? require('next/dist/compiled/bytes').parse(bodySizeLimit) : 1024 * 1024 // 1 MB | |
| ; | |
| let size = 0; | |
| const sizeLimitTransform = new Transform({ | |
| transform (chunk, encoding, callback) { | |
| size += Buffer.byteLength(chunk, encoding); | |
| if (size > bodySizeLimitBytes) { | |
| const { ApiError } = require('../api-utils'); | |
| callback(Object.defineProperty(new ApiError(413, `Body exceeded ${bodySizeLimit} limit.\n` + `To configure the body size limit for Server Actions, see: https://nextjs.org/docs/app/api-reference/next-config-js/serverActions#bodysizelimit`), "__NEXT_ERROR_CODE", { | |
| value: "E394", | |
| enumerable: false, | |
| configurable: true | |
| })); | |
| return; | |
| } | |
| callback(null, chunk); | |
| } | |
| }); | |
| if (isMultipartAction) { | |
| if (isFetchAction) { | |
| // A fetch action with a multipart body. | |
| try { | |
| actionModId = getActionModIdOrError(actionId, serverModuleMap); | |
| } catch (err) { | |
| return handleUnrecognizedFetchAction(err); | |
| } | |
| const busboy = require('next/dist/compiled/busboy')({ | |
| defParamCharset: 'utf8', | |
| headers: req.headers, | |
| limits: { | |
| fieldSize: bodySizeLimitBytes | |
| } | |
| }); | |
| const abortController = new AbortController(); | |
| try { | |
| ; | |
| [, boundActionArguments] = await Promise.all([ | |
| pipeline(req.body, sizeLimitTransform, busboy, { | |
| signal: abortController.signal | |
| }), | |
| decodeReplyFromBusboy(busboy, serverModuleMap, { | |
| temporaryReferences | |
| }) | |
| ]); | |
| } catch (err) { | |
| abortController.abort(); | |
| throw err; | |
| } | |
| } else { | |
| // Multipart POST, but not a fetch action. | |
| // Potentially an MPA action, we have to try decoding it to check. | |
| const sizeLimitedBody = new PassThrough(); | |
| // React doesn't yet publish a busboy version of decodeAction | |
| // so we polyfill the parsing of FormData. | |
| const fakeRequest = new Request('http://localhost', { | |
| method: 'POST', | |
| // @ts-expect-error | |
| headers: { | |
| 'Content-Type': contentType | |
| }, | |
| body: Readable.toWeb(sizeLimitedBody), | |
| duplex: 'half' | |
| }); | |
| let formData; | |
| const abortController = new AbortController(); | |
| try { | |
| ; | |
| [, formData] = await Promise.all([ | |
| pipeline(req.body, sizeLimitTransform, sizeLimitedBody, { | |
| signal: abortController.signal | |
| }), | |
| fakeRequest.formData() | |
| ]); | |
| } catch (err) { | |
| abortController.abort(); | |
| throw err; | |
| } | |
| if (areAllActionIdsValid(formData, serverModuleMap) === false) { | |
| // TODO: This can be from skew or manipulated input. We should handle this case | |
| // more gracefully but this preserves the prior behavior where decodeAction would throw instead. | |
| throw Object.defineProperty(new Error(`Failed to find Server Action. This request might be from an older or newer deployment.\nRead more: https://nextjs.org/docs/messages/failed-to-find-server-action`), "__NEXT_ERROR_CODE", { | |
| value: "E975", | |
| enumerable: false, | |
| configurable: true | |
| }); | |
| } | |
| // TODO: Refactor so it is harder to accidentally decode an action before you have validated that the | |
| // action referred to is available. | |
| const action = await decodeAction(formData, serverModuleMap); | |
| if (typeof action === 'function') { | |
| // an MPA action. | |
| // Only warn if it's a server action, otherwise skip for other post requests | |
| warnBadServerActionRequest(); | |
| const { actionResult } = await executeActionAndPrepareForRender(action, [], workStore, requestStore, actionWasForwarded); | |
| const formState = await decodeFormState(actionResult, formData, serverModuleMap); | |
| // Skip the fetch path. | |
| // We need to render a full HTML version of the page for the response, we'll handle that in app-render. | |
| return { | |
| type: 'done', | |
| result: undefined, | |
| formState | |
| }; | |
| } else { | |
| // We couldn't decode an action, so this POST request turned out not to be a server action request. | |
| return null; | |
| } | |
| } | |
| } else { | |
| // POST with non-multipart body. | |
| // If it's not multipart AND not a fetch action, | |
| // then it can't be an action request. | |
| if (!isFetchAction) { | |
| return null; | |
| } | |
| try { | |
| actionModId = getActionModIdOrError(actionId, serverModuleMap); | |
| } catch (err) { | |
| return handleUnrecognizedFetchAction(err); | |
| } | |
| // A fetch action with a non-multipart body. | |
| // In practice, this happens if `encodeReply` returned a string instead of FormData, | |
| // which can happen for very simple JSON-like values that don't need multiple flight rows. | |
| const sizeLimitedBody = new PassThrough(); | |
| const chunks = []; | |
| await Promise.all([ | |
| pipeline(req.body, sizeLimitTransform, sizeLimitedBody), | |
| (async ()=>{ | |
| for await (const chunk of sizeLimitedBody){ | |
| chunks.push(Buffer.from(chunk)); | |
| } | |
| })() | |
| ]); | |
| const actionData = Buffer.concat(chunks).toString('utf-8'); | |
| boundActionArguments = await decodeReply(actionData, serverModuleMap, { | |
| temporaryReferences | |
| }); | |
| } | |
| } else { | |
| throw Object.defineProperty(new Error('Invariant: Unknown request type.'), "__NEXT_ERROR_CODE", { | |
| value: "E114", | |
| enumerable: false, | |
| configurable: true | |
| }); | |
| } | |
| // actions.js | |
| // app/page.js | |
| // action worker1 | |
| // appRender1 | |
| // app/foo/page.js | |
| // action worker2 | |
| // appRender | |
| // / -> fire action -> POST / -> appRender1 -> modId for the action file | |
| // /foo -> fire action -> POST /foo -> appRender2 -> modId for the action file | |
| const actionMod = await ComponentMod.__next_app__.require(actionModId); | |
| const actionHandler = actionMod[// `actionId` must exist if we got here, as otherwise we would have thrown an error above | |
| actionId]; | |
| const { actionResult, skipPageRendering } = await executeActionAndPrepareForRender(actionHandler, boundActionArguments, workStore, requestStore, actionWasForwarded).finally(()=>{ | |
| addRevalidationHeader(res, { | |
| workStore, | |
| requestStore | |
| }); | |
| }); | |
| // For form actions, we need to continue rendering the page. | |
| if (isFetchAction) { | |
| return { | |
| type: 'done', | |
| result: await generateFlight(req, ctx, requestStore, { | |
| actionResult: Promise.resolve(actionResult), | |
| skipPageRendering, | |
| temporaryReferences, | |
| // If we skip page rendering, we need to ensure pending | |
| // revalidates are awaited before closing the response. Otherwise, | |
| // this will be done after rendering the page. | |
| waitUntil: skipPageRendering ? (0, _revalidationutils.executeRevalidates)(workStore) : undefined | |
| }) | |
| }; | |
| } else { | |
| // TODO: this shouldn't be reachable, because all non-fetch codepaths return early. | |
| // this will be handled in a follow-up refactor PR. | |
| return null; | |
| } | |
| }); | |
| } catch (err) { | |
| if ((0, _redirecterror.isRedirectError)(err)) { | |
| const redirectUrl = (0, _redirect.getURLFromRedirectError)(err); | |
| const redirectType = (0, _redirect.getRedirectTypeFromError)(err); | |
| // if it's a fetch action, we'll set the status code for logging/debugging purposes | |
| // but we won't set a Location header, as the redirect will be handled by the client router | |
| res.statusCode = _redirectstatuscode.RedirectStatusCode.SeeOther; | |
| metadata.statusCode = _redirectstatuscode.RedirectStatusCode.SeeOther; | |
| if (isFetchAction) { | |
| return { | |
| type: 'done', | |
| result: await createRedirectRenderResult(req, res, host, redirectUrl, redirectType, ctx.renderOpts.basePath, workStore, requestStore.url.pathname) | |
| }; | |
| } | |
| // For an MPA action, the redirect doesn't need a body, just a Location header. | |
| res.setHeader('Location', redirectUrl); | |
| return { | |
| type: 'done', | |
| result: _renderresult.default.EMPTY | |
| }; | |
| } else if ((0, _httpaccessfallback.isHTTPAccessFallbackError)(err)) { | |
| res.statusCode = (0, _httpaccessfallback.getAccessFallbackHTTPStatus)(err); | |
| metadata.statusCode = res.statusCode; | |
| if (isFetchAction) { | |
| const promise = Promise.reject(err); | |
| try { | |
| // we need to await the promise to trigger the rejection early | |
| // so that it's already handled by the time we call | |
| // the RSC runtime. Otherwise, it will throw an unhandled | |
| // promise rejection error in the renderer. | |
| await promise; | |
| } catch { | |
| // swallow error, it's gonna be handled on the client | |
| } | |
| return { | |
| type: 'done', | |
| result: await generateFlight(req, ctx, requestStore, { | |
| skipPageRendering: false, | |
| actionResult: promise, | |
| temporaryReferences | |
| }) | |
| }; | |
| } | |
| // For an MPA action, we need to render a HTML response. We'll handle that in app-render. | |
| return { | |
| type: 'not-found' | |
| }; | |
| } | |
| // An error that didn't come from `redirect()` or `notFound()`, likely thrown from user code | |
| // (but it could also be a bug in our code!) | |
| if (isFetchAction) { | |
| // TODO: consider checking if the error is an `ApiError` and change status code | |
| // so that we can respond with a 413 to requests that break the body size limit | |
| // (but if we do that, we also need to make sure that whatever handles the non-fetch error path below does the same) | |
| res.statusCode = 500; | |
| metadata.statusCode = 500; | |
| const promise = Promise.reject(err); | |
| try { | |
| // we need to await the promise to trigger the rejection early | |
| // so that it's already handled by the time we call | |
| // the RSC runtime. Otherwise, it will throw an unhandled | |
| // promise rejection error in the renderer. | |
| await promise; | |
| } catch { | |
| // swallow error, it's gonna be handled on the client | |
| } | |
| return { | |
| type: 'done', | |
| result: await generateFlight(req, ctx, requestStore, { | |
| actionResult: promise, | |
| // If the page was not revalidated, or if the action was forwarded | |
| // from another worker, we can skip rendering the page. | |
| skipPageRendering: workStore.pathWasRevalidated === undefined || workStore.pathWasRevalidated === _actionrevalidationkind.ActionDidNotRevalidate || actionWasForwarded, | |
| temporaryReferences | |
| }) | |
| }; | |
| } | |
| // For an MPA action, we need to render a HTML response. We'll rethrow the error and let it be handled above. | |
| throw err; | |
| } | |
| } | |
| async function executeActionAndPrepareForRender(action, args, workStore, requestStore, actionWasForwarded) { | |
| requestStore.phase = 'action'; | |
| let skipPageRendering = actionWasForwarded; | |
| try { | |
| const actionResult = await _workunitasyncstorageexternal.workUnitAsyncStorage.run(requestStore, ()=>action.apply(null, args)); | |
| // If the page was not revalidated, or if the action was forwarded from | |
| // another worker, we can skip rendering the page. | |
| skipPageRendering ||= workStore.pathWasRevalidated === undefined || workStore.pathWasRevalidated === _actionrevalidationkind.ActionDidNotRevalidate; | |
| return { | |
| actionResult, | |
| skipPageRendering | |
| }; | |
| } finally{ | |
| if (!skipPageRendering) { | |
| requestStore.phase = 'render'; | |
| // When we switch to the render phase, cookies() will return | |
| // `workUnitStore.cookies` instead of | |
| // `workUnitStore.userspaceMutableCookies`. We want the render to see any | |
| // cookie writes that we performed during the action, so we need to update | |
| // the immutable cookies to reflect the changes. | |
| (0, _requeststore.synchronizeMutableCookies)(requestStore); | |
| // The server action might have toggled draft mode, so we need to reflect | |
| // that in the work store to be up-to-date for subsequent rendering. | |
| workStore.isDraftMode = requestStore.draftMode.isEnabled; | |
| // If the action called revalidateTag/revalidatePath, then that might | |
| // affect data used by the subsequent render, so we need to make sure all | |
| // revalidations are applied before that. | |
| await (0, _revalidationutils.executeRevalidates)(workStore); | |
| } | |
| } | |
| } | |
| /** | |
| * Attempts to find the module ID for the action from the module map. When this fails, it could be a deployment skew where | |
| * the action came from a different deployment. It could also simply be an invalid POST request that is not a server action. | |
| * In either case, we'll throw an error to be handled by the caller. | |
| */ function getActionModIdOrError(actionId, serverModuleMap) { | |
| var _serverModuleMap_actionId; | |
| // if we're missing the action ID header, we can't do any further processing | |
| if (!actionId) { | |
| throw Object.defineProperty(new _invarianterror.InvariantError("Missing 'next-action' header."), "__NEXT_ERROR_CODE", { | |
| value: "E664", | |
| enumerable: false, | |
| configurable: true | |
| }); | |
| } | |
| const actionModId = (_serverModuleMap_actionId = serverModuleMap[actionId]) == null ? void 0 : _serverModuleMap_actionId.id; | |
| if (!actionModId) { | |
| throw getActionNotFoundError(actionId); | |
| } | |
| return actionModId; | |
| } | |
| function getActionNotFoundError(actionId) { | |
| return Object.defineProperty(new Error(`Failed to find Server Action${actionId ? ` "${actionId}"` : ''}. This request might be from an older or newer deployment.\nRead more: https://nextjs.org/docs/messages/failed-to-find-server-action`), "__NEXT_ERROR_CODE", { | |
| value: "E974", | |
| enumerable: false, | |
| configurable: true | |
| }); | |
| } | |
| const $ACTION_ = '$ACTION_'; | |
| const $ACTION_REF_ = '$ACTION_REF_'; | |
| const $ACTION_ID_ = '$ACTION_ID_'; | |
| const ACTION_ID_EXPECTED_LENGTH = 42; | |
| /** | |
| * This function mirrors logic inside React's decodeAction and should be kept in sync with that. | |
| * It pre-parses the FormData to ensure that any action IDs referred to are actual action IDs for | |
| * this Next.js application. | |
| */ function areAllActionIdsValid(mpaFormData, serverModuleMap) { | |
| let hasAtLeastOneAction = false; | |
| // Before we attempt to decode the payload for a possible MPA action, assert that all | |
| // action IDs are valid IDs. If not we should disregard the payload | |
| for (let key of mpaFormData.keys()){ | |
| if (!key.startsWith($ACTION_)) { | |
| continue; | |
| } | |
| if (key.startsWith($ACTION_ID_)) { | |
| // No Bound args case | |
| if (isInvalidActionIdFieldName(key, serverModuleMap)) { | |
| return false; | |
| } | |
| hasAtLeastOneAction = true; | |
| } else if (key.startsWith($ACTION_REF_)) { | |
| // Bound args case | |
| const actionDescriptorField = $ACTION_ + key.slice($ACTION_REF_.length) + ':0'; | |
| const actionFields = mpaFormData.getAll(actionDescriptorField); | |
| if (actionFields.length !== 1) { | |
| return false; | |
| } | |
| const actionField = actionFields[0]; | |
| if (typeof actionField !== 'string') { | |
| return false; | |
| } | |
| if (isInvalidStringActionDescriptor(actionField, serverModuleMap)) { | |
| return false; | |
| } | |
| hasAtLeastOneAction = true; | |
| } | |
| } | |
| return hasAtLeastOneAction; | |
| } | |
| const ACTION_DESCRIPTOR_ID_PREFIX = '{"id":"'; | |
| function isInvalidStringActionDescriptor(actionDescriptor, serverModuleMap) { | |
| if (actionDescriptor.startsWith(ACTION_DESCRIPTOR_ID_PREFIX) === false) { | |
| return true; | |
| } | |
| const from = ACTION_DESCRIPTOR_ID_PREFIX.length; | |
| const to = from + ACTION_ID_EXPECTED_LENGTH; | |
| // We expect actionDescriptor to be '{"id":"<actionId>",...}' | |
| const actionId = actionDescriptor.slice(from, to); | |
| if (actionId.length !== ACTION_ID_EXPECTED_LENGTH || actionDescriptor[to] !== '"') { | |
| return true; | |
| } | |
| const entry = serverModuleMap[actionId]; | |
| if (entry == null) { | |
| return true; | |
| } | |
| return false; | |
| } | |
| function isInvalidActionIdFieldName(actionIdFieldName, serverModuleMap) { | |
| // The field name must always start with $ACTION_ID_ but since it is | |
| // the id is extracted from the key of the field we have already validated | |
| // this before entering this function | |
| if (actionIdFieldName.length !== $ACTION_ID_.length + ACTION_ID_EXPECTED_LENGTH) { | |
| // this field name has too few or too many characters | |
| return true; | |
| } | |
| const actionId = actionIdFieldName.slice($ACTION_ID_.length); | |
| const entry = serverModuleMap[actionId]; | |
| if (entry == null) { | |
| return true; | |
| } | |
| return false; | |
| } | |
| //# sourceMappingURL=action-handler.js.map |