Skip to content

refactor: extract shared request handling from server entry points#258

Open
yunus25jmi1 wants to merge 1 commit intocloudflare:mainfrom
yunus25jmi1:refactor/shared-request-handling
Open

refactor: extract shared request handling from server entry points#258
yunus25jmi1 wants to merge 1 commit intocloudflare:mainfrom
yunus25jmi1:refactor/shared-request-handling

Conversation

@yunus25jmi1
Copy link
Contributor

Summary

Deduplicates ~250 lines of inline request-handling logic from the generated App Router RSC entry (\�pp-dev-server.ts) and Pages Router server entry (\index.ts) by importing from the canonical \config-matchers.ts\ and a new
equest-pipeline.ts\ module.

Changes

New file: \server/request-pipeline.ts\

Shared request lifecycle utilities extracted for reuse across entry points:

  • \guardProtocolRelativeUrl()\ — blocks //evil.com\ protocol-relative URL attacks
  • \stripBasePath()\ — removes configured basePath prefix from pathnames

  • ormalizeTrailingSlash()\ — redirects to canonical trailing-slash form
  • \�alidateCsrfOrigin()\ — CSRF origin validation for server actions
  • \isOriginAllowed()\ — wildcard subdomain matching for allowed origins
  • \�alidateImageUrl()\ — image optimization URL validation
  • \processMiddlewareHeaders()\ — processes middleware response headers (redirects, rewrites, status overrides)

\�pp-dev-server.ts\ (~250 lines removed)

Replaced 14 inline __-prefixed functions with imports from \config-matchers.ts\ and
equest-pipeline.ts:

  • Config matching: \matchRedirect, \matchRewrite, \matchHeaders, \sanitizeDestination\
  • External proxying: \isExternalUrl, \proxyExternalRequest\
  • Request context:
    equestContextFromRequest\
  • Security: \�alidateCsrfOrigin, \�alidateImageUrl\

\index.ts\

  • Replaced inline \parseCookies\ in generated server entry template with import from \config-matchers.ts\
  • Replaced manual \RequestContext\ construction with
    equestContextFromRequest()\
  • Removed duplicate \sanitizeDestinationLocal(), using imported \sanitizeDestination()\
  • Removed duplicate \matchConfigPattern\ + \�xtractConstraint\ (~100 lines), re-exporting from \config-matchers.ts\

Test updates

Updated \�pp-router.test.ts\ assertions to reference the new imported function names instead of old inline __-prefixed names.

Security Fixes

  • matchConfigPattern: eliminates chained .replace()\ divergence (CodeQL flagged incomplete sanitization) in favor of config-matchers single-pass tokenizer
  • proxyExternalRequest: uses \AbortController+\setTimeout\ (broader runtime support) instead of \AbortSignal.timeout()\
  • escapeHeaderSource: uses proper config-matchers implementation instead of simplified chained .replace()\

Verification

  • Typecheck: \pnpm run typecheck\ passes clean
  • Shims tests: 537/537 passed
  • App Router tests: 192/192 passed
  • Pre-existing failures on main (Windows path issues in image-imports, routing, and pages-router tests) confirmed not related to this change

Closes #254

@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 5, 2026

Open in StackBlitz

npm i https://pkg.pr.new/vinext@258

commit: 8fcb679

@yunus25jmi1 yunus25jmi1 force-pushed the refactor/shared-request-handling branch from 14ccb24 to 631e3fc Compare March 5, 2026 02:19
@yunus25jmi1
Copy link
Contributor Author

yunus25jmi1 commented Mar 5, 2026

All checks have passed with all green. Kindly review the changes. @southpolesteve @elithrar

@yunus25jmi1 yunus25jmi1 force-pushed the refactor/shared-request-handling branch 2 times, most recently from 2cb7c43 to 6624adc Compare March 8, 2026 08:10
@yunus25jmi1
Copy link
Contributor Author

@southpolesteve review the changes. Conflicts resolved.

@james-elicx
Copy link
Collaborator

/bigbonk review

Copy link
Contributor

@ask-bonk ask-bonk bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: Extract shared request handling from server entry points

The direction of this PR is right — deduplicating the inline __-prefixed helpers from the generated code into importable modules is a meaningful improvement. Having a single canonical matchConfigPattern (single-pass tokenizer) instead of the chained .replace() variant is a real fix.

However, there are several issues that need to be addressed before this can merge.

Key Issues

  1. Dead code in request-pipeline.ts — Four of the seven exported functions (guardProtocolRelativeUrl, stripBasePath, normalizeTrailingSlash, processMiddlewareHeaders) are never imported or called anywhere in the codebase. The protocol-relative guard is still inlined in app-dev-server.ts at line 1125. This new module ships dead code that increases the bundle without any deduplication benefit. Either wire up these functions at the call sites that have the inline versions, or remove them from this PR and add them when they're actually used.

  2. Behavioral difference in validateImageUrl — The old code resolved the image URL against request.url (full URL with path+query), while the new code resolves against url.origin (scheme+host only). For root-relative paths like /images/foo.png this is equivalent, but the origin check inside validateImageUrl resolves against url.origin too (line 190), which means the defense-in-depth check is comparing url.origin === url.origin — it's a tautology. This was also a tautology in the original code (since /images/foo.png resolved against the same origin will always keep the same origin), so the security posture is unchanged, but it would be good to document why this defense-in-depth check exists despite being tautological for the filtered inputs.

  3. Extra blank lines in generated code — The removal of the inline functions left orphaned blank lines in the generated entry at two locations (lines 1049-1051 and 1173-1176). Minor, but noticeable in the generated output.

  4. safeRegExp removed from imports but was previously used in index.ts — The diff removes safeRegExp from the config-matchers.js import in index.ts (line 29-38). Verify this isn't used elsewhere in the file or re-exported for consumers.

Correctness Observations

  • The validateCsrfOrigin signature change from closure-captured __allowedOrigins to an explicit parameter is clean and correct. The call site at line 1352 passes __allowedOrigins explicitly.
  • The matchRedirect/matchRewrite/matchHeaders functions in config-matchers.ts take individual rule arrays, which matches how they're called in the generated code (e.g., matchRedirect(__redirPathname, __configRedirects, __reqCtx)).
  • The requestContextFromRequest replacement for __buildRequestContext is a straightforward 1:1 swap with the same semantics.
  • The matchConfigPattern re-export from index.ts (line 3749) preserves the public API for external consumers.
  • Test updates correctly reflect the new function names and are otherwise semantically equivalent.

Suggestions

See inline comments.

return new Response("404 Not Found", { status: 404 });
}
return null;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

guardProtocolRelativeUrl is exported but never imported anywhere. The protocol-relative URL guard is still inlined in app-dev-server.ts at line 1125:

if (url.pathname.replaceAll("\\\\" , "/").startsWith("//")) {

Either replace the inline check with this function, or remove it from this PR. Shipping dead code in a module intended to deduplicate code is counterproductive.

return pathname.slice(basePath.length) || "/";
}
return pathname;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stripBasePath is exported but never imported anywhere in the codebase. The inline pathname.slice(basePath.length) || "/" pattern is still used directly in app-dev-server.ts (line 1141) and index.ts. Same concern as guardProtocolRelativeUrl — either wire it up or defer to a follow-up PR.

});
}
return null;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

normalizeTrailingSlash is exported but never called. The trailing slash logic is still inlined in app-dev-server.ts at lines 1146-1153. Same issue.

for (const key of keysToDelete) {
headers.delete(key);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

processMiddlewareHeaders is exported but never imported. The middleware header unpacking is still done inline in app-dev-server.ts at lines 1260-1267. This is the fourth unused function in this module.

To summarize: only validateCsrfOrigin and validateImageUrl from this file are actually wired up. The other four functions are dead code.

}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stray extra blank lines left behind from removing the inline functions. Two consecutive blank lines here; please collapse to one.

Suggested change

});
return new Response(upstream.body, { status: upstream.status, statusText: upstream.statusText, headers: respHeaders });
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue — two consecutive blank lines left over from the removal of __proxyExternalRequest and __applyConfigHeaders. Please collapse.

Suggested change

Comment on lines +188 to +194
// Validate the constructed URL's origin hasn't changed (defense in depth).
const url = new URL(requestUrl);
const resolvedImg = new URL(imgUrl, url.origin);
if (resolvedImg.origin !== url.origin) {
return new Response("Only relative URLs allowed", { status: 400 });
}
return imgUrl;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The defense-in-depth origin check is tautological for all inputs that pass the guard above (must start with / but not //). Resolving a root-relative path against url.origin will always produce the same origin. This was also true in the original inline code, but since this is now a standalone function with its own documentation, it would be helpful to add a comment explaining that this check exists as a safety net against future changes to the guard logic, not because it catches anything today.

import { safeJsonStringify } from "vinext/html";
import { getSSRFontLinks as _getSSRFontLinks, getSSRFontStyles as _getSSRFontStylesGoogle, getSSRFontPreloads as _getSSRFontPreloadsGoogle } from "next/font/google";
import { getSSRFontStyles as _getSSRFontStylesLocal, getSSRFontPreloads as _getSSRFontPreloadsLocal } from "next/font/local";
import { parseCookies } from ${JSON.stringify(path.resolve(__dirname, "config/config-matchers.js").replace(/\\/g, "/"))};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This import path uses path.resolve(__dirname, ...) which produces a platform-dependent absolute path. The existing pattern in the file already does this for other generated imports, so it's consistent. Just noting that this follows the existing convention.

@ask-bonk
Copy link
Contributor

ask-bonk bot commented Mar 8, 2026

Review submitted with REQUEST_CHANGES status. Here's a summary of the key findings:

The PR's direction is sound — deduplicating ~250 lines of inline __-prefixed helpers into shared modules is a real improvement, especially replacing the chained .replace() matchConfigPattern with the canonical single-pass tokenizer from config-matchers.ts.

Issues flagged:

  1. Dead code — 4 of 7 functions in the new request-pipeline.ts (guardProtocolRelativeUrl, stripBasePath, normalizeTrailingSlash, processMiddlewareHeaders) are never imported anywhere. The inline versions still exist in the generated code. These should either be wired up or deferred.

  2. validateImageUrl origin check is a tautology for filtered inputs (always same-origin after the /-but-not-// guard). Not a regression, but worth documenting since it's now a standalone function.

  3. Stray double blank lines left behind at two locations in the generated code.

  4. safeRegExp removed from imports in index.ts — needs verification it's not used elsewhere in that file.

Correctness is solid for the parts that are actually wired up: validateCsrfOrigin, validateImageUrl, matchRedirect/matchRewrite/matchHeaders, requestContextFromRequest, sanitizeDestination, and matchConfigPattern re-export all have correct signatures and call sites.

github run

…quest-pipeline

Deduplicate ~250 lines of inline request-handling logic from the generated
App Router RSC entry (app-dev-server.ts) and Pages Router server entry
(index.ts) by importing from the canonical config-matchers.ts and a new
request-pipeline.ts module.

Changes:
- Create server/request-pipeline.ts with shared utilities:
  guardProtocolRelativeUrl, stripBasePath, normalizeTrailingSlash,
  validateCsrfOrigin, isOriginAllowed, validateImageUrl,
  processMiddlewareHeaders
- app-dev-server.ts: replace 14 inline __-prefixed functions with imports
  from config-matchers.ts (matchRedirect, matchRewrite, matchHeaders,
  isExternalUrl, proxyExternalRequest, sanitizeDestination,
  requestContextFromRequest) and request-pipeline.ts (validateCsrfOrigin,
  validateImageUrl)
- index.ts: replace inline parseCookies, sanitizeDestinationLocal, and
  matchConfigPattern with imports/re-exports from config-matchers.ts;
  replace manual RequestContext construction with requestContextFromRequest()
- Update tests to reference imported function names instead of old inline
  __-prefixed names

Security fixes:
- matchConfigPattern: eliminates chained .replace() divergence (CodeQL
  flagged) in favor of config-matchers single-pass tokenizer
- proxyExternalRequest: uses AbortController+setTimeout (broader runtime
  support) instead of AbortSignal.timeout()
- escapeHeaderSource: uses proper config-matchers implementation instead of
  simplified chained .replace()

Closes cloudflare#254

t status
@yunus25jmi1 yunus25jmi1 force-pushed the refactor/shared-request-handling branch from 6624adc to 439ebd3 Compare March 8, 2026 10:25
@yunus25jmi1
Copy link
Contributor Author

@james-elicx Re-review the changes. Kindly merge the PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor: Extract shared request handling logic from the four server entry points

2 participants