import type { AppLoadContext } from "./data" ; import { callRouteAction , callRouteLoader , extractData } from "./data" ; import type { AppState } from "./errors" ; import type { HandleDataRequestFunction , ServerBuild } from "./build" ; import type { EntryContext } from "./entry" ; import { createEntryMatches , createEntryRouteModules } from "./entry" ; import { serializeError } from "./errors" ; import { getDocumentHeaders } from "./headers" ; import type { ServerPlatform } from "./platform" ; import type { RouteMatch } from "./routeMatching" ; import { matchServerRoutes } from "./routeMatching" ; import { ServerMode , isServerMode } from "./mode" ; import type { ServerRoute } from "./routes" ; import { createRoutes } from "./routes" ; import { json , isRedirectResponse , isCatchResponse } from "./responses" ; import { createServerHandoffString } from "./serverHandoff" ; /**
 * The main request handler for a Remix server. This handler runs in the context
 * of a cloud provider's server (e.g. Express on Firebase) or locally via their
 * dev tools.
 */ export interface RequestHandler { ( request , loadContext ? ) : Promise < Response > ; } /**
 * Creates a function that serves HTTP requests.
 */ export function createRequestHandler ( build , platform , mode ? ) { let routes = createRoutes ( build . routes ) ; let serverMode = isServerMode ( mode ) ? mode : ServerMode . Production ; return async function requestHandler ( request , loadContext ) { let url = new URL ( request . url ) ; let matches = matchServerRoutes ( routes , url . pathname ) ; let requestType = getRequestType ( url , matches ) ; let response ; switch ( requestType ) { case "data" : response = await handleDataRequest ( { request , loadContext , matches : matches ! , handleDataRequest : build . entry . module . handleDataRequest , serverMode } ) ; break ; case "document" : response = await renderDocumentRequest ( { build , loadContext , matches , request , routes , serverMode } ) ; break ; case "resource" : response = await handleResourceRequest ( { request , loadContext , matches : matches ! , serverMode } ) ; break ; } if ( request . method . toLowerCase ( ) === "head" ) { return new Response ( null , { headers : response . headers , status : response . status , statusText : response . statusText } ) ; } return response ; } ; } async function handleDataRequest ( { handleDataRequest , loadContext , matches , request , serverMode } ) { if ( ! isValidRequestMethod ( request ) ) { return errorBoundaryError ( new Error ( `Invalid request method "${ request . method }"` ) , 405 ) ; } let url = new URL ( request . url ) ; if ( ! matches ) { return errorBoundaryError ( new Error ( `No route matches URL "${ url . pathname }"` ) , 404 ) ; } let response ; let match ; try { if ( isActionRequest ( request ) ) { match = getActionRequestMatch ( url , matches ) ; response = await callRouteAction ( { loadContext , match , request : request } ) ; } else { let routeId = url . searchParams . get ( "_data" ) ; if ( ! routeId ) { return errorBoundaryError ( new Error ( `Missing route id in ?_data` ) , 403 ) ; } let tempMatch = matches . find ( match => match . route . id === routeId ) ; if ( ! tempMatch ) { return errorBoundaryError ( new Error ( `Route "${ routeId }" does not match URL "${ url . pathname }"` ) , 403 ) ; } match = tempMatch ; response = await callRouteLoader ( { loadContext , match , request } ) ; } if ( isRedirectResponse ( response ) ) { let headers = new Headers ( response . headers ) ; headers . set ( "X-Remix-Redirect" , headers . get ( "Location" ) ! ) ; headers . delete ( "Location" ) ; return new Response ( null , { status : 204 , headers } ) ; } if ( handleDataRequest ) { response = await handleDataRequest ( response . clone ( ) , { context : loadContext , params : match . params , request : request . clone ( ) } ) ; } return response ; } catch ( error ) { if ( serverMode !== ServerMode . Test ) { console . error ( error ) ; } if ( serverMode === ServerMode . Development ) { return errorBoundaryError ( error as Error , 500 ) ; } return errorBoundaryError ( new Error ( "Unexpected Server Error" ) , 500 ) ; } } async function renderDocumentRequest ( { build , loadContext , matches , request , routes , serverMode } ) { let url = new URL ( request . url ) ; let appState = { trackBoundaries : true , trackCatchBoundaries : true , catchBoundaryRouteId : null , renderBoundaryRouteId : null , loaderBoundaryRouteId : null , error : undefined , catch : undefined } ; if ( ! isValidRequestMethod ( request ) ) { matches = null ; appState . trackCatchBoundaries = false ; appState . catch = { data : null , status : 405 , statusText : "Method Not Allowed" } ; } else if ( ! matches ) { appState . trackCatchBoundaries = false ; appState . catch = { data : null , status : 404 , statusText : "Not Found" } ; } let actionStatus ; let actionData ; let actionMatch ; let actionResponse ; if ( matches && isActionRequest ( request ) ) { actionMatch = getActionRequestMatch ( url , matches ) ; try { actionResponse = await callRouteAction ( { loadContext , match : actionMatch , request : request } ) ; if ( isRedirectResponse ( actionResponse ) ) { return actionResponse ; } actionStatus = { status : actionResponse . status , statusText : actionResponse . statusText } ; if ( isCatchResponse ( actionResponse ) ) { appState . catchBoundaryRouteId = getDeepestRouteIdWithBoundary ( matches , "CatchBoundary" ) ; appState . trackCatchBoundaries = false ; appState . catch = { ... actionStatus , data : await extractData ( actionResponse ) } ; } else { actionData = { [ actionMatch . route . id ] : await extractData ( actionResponse ) } ; } } catch ( error ) { appState . loaderBoundaryRouteId = getDeepestRouteIdWithBoundary ( matches , "ErrorBoundary" ) ; appState . trackBoundaries = false ; appState . error = await serializeError ( error ) ; if ( serverMode !== ServerMode . Test ) { console . error ( `There was an error running the action for route ${ actionMatch . route . id }` ) ; } } } let routeModules = createEntryRouteModules ( build . routes ) ; let matchesToLoad = matches || [ ] ; if ( appState . catch ) { matchesToLoad = getMatchesUpToDeepestBoundary ( matchesToLoad . slice ( 0 , - 1 ) , "CatchBoundary" ) ; } else if ( appState . error ) { matchesToLoad = getMatchesUpToDeepestBoundary ( matchesToLoad . slice ( 0 , - 1 ) , "ErrorBoundary" ) ; } let routeLoaderResults = await Promise . allSettled ( matchesToLoad . map ( match => match . route . module . loader ? callRouteLoader ( { loadContext , match , request } ) : Promise . resolve ( undefined ) ) ) ; let actionCatch = appState . catch ; let actionError = appState . error ; let actionCatchBoundaryRouteId = appState . catchBoundaryRouteId ; let actionLoaderBoundaryRouteId = appState . loaderBoundaryRouteId ; appState . catch = undefined ; appState . error = undefined ; let headerMatches = [ ] ; let routeLoaderResponses = [ ] ; let loaderStatusCodes = [ ] ; let routeData = { } ; for ( let index = 0 ; index < matchesToLoad . length ; index ++ ) { let match = matchesToLoad [ index ] ; let result = routeLoaderResults [ index ] ; let error = result . status === "rejected" ? result . reason : undefined ; let response = result . status === "fulfilled" ? result . value : undefined ; let isRedirect = response ? isRedirectResponse ( response ) : false ; let isCatch = response ? isCatchResponse ( response ) : false ; if ( appState . catch || appState . error ) { break ; } if ( ! actionCatch && ! actionError && response && isRedirect ) { return response ; } if ( match . route . module . CatchBoundary ) { appState . catchBoundaryRouteId = match . route . id ; } if ( match . route . module . ErrorBoundary ) { appState . loaderBoundaryRouteId = match . route . id ; } if ( error ) { loaderStatusCodes . push ( 500 ) ; appState . trackBoundaries = false ; appState . error = await serializeError ( error ) ; if ( serverMode !== ServerMode . Test ) { console . error ( `There was an error running the data loader for route ${ match . route . id }` ) ; } break ; } else if ( response ) { headerMatches . push ( match ) ; routeLoaderResponses . push ( response ) ; loaderStatusCodes . push ( response . status ) ; if ( isCatch ) { appState . trackCatchBoundaries = false ; appState . catch = { data : await extractData ( response ) , status : response . status , statusText : response . statusText } ; break ; } else { routeData [ match . route . id ] = await extractData ( response ) ; } } } if ( ! appState . catch ) { appState . catchBoundaryRouteId = actionCatchBoundaryRouteId ; } if ( ! appState . error ) { appState . loaderBoundaryRouteId = actionLoaderBoundaryRouteId ; } appState . catch = actionCatch || appState . catch ; appState . error = actionError || appState . error ; let renderableMatches = getRenderableMatches ( matches , appState ) ; if ( ! renderableMatches ) { renderableMatches = [ ] ; let root = routes [ 0 ] ; if ( root ?. module . CatchBoundary ) { appState . catchBoundaryRouteId = "root" ; renderableMatches . push ( { params : { } , pathname : "" , route : routes [ 0 ] } ) ; } } let notOkResponse = actionStatus && actionStatus . status !== 200 ? actionStatus . status : loaderStatusCodes . find ( status => status !== 200 ) ; let responseStatusCode = appState . error ? 500 : typeof notOkResponse === "number" ? notOkResponse : appState . catch ? appState . catch . status : 200 ; let responseHeaders = getDocumentHeaders ( build , renderableMatches , routeLoaderResponses , actionResponse ) ; let entryMatches = createEntryMatches ( renderableMatches , build . assets . routes ) ; let serverHandoff = { actionData , appState : appState , matches : entryMatches , routeData } ; let entryContext = { ... serverHandoff , manifest : build . assets , routeModules , serverHandoffString : createServerHandoffString ( serverHandoff ) } ; let handleDocumentRequest = build . entry . module . default ; try { return await handleDocumentRequest ( request . clone ( ) , responseStatusCode , responseHeaders , entryContext ) ; } catch ( error ) { responseStatusCode = 500 ; appState . trackBoundaries = false ; appState . error = await serializeError ( error ) ; entryContext . serverHandoffString = createServerHandoffString ( serverHandoff ) ; try { return await handleDocumentRequest ( request . clone ( ) , responseStatusCode , responseHeaders , entryContext ) ; } catch ( error ) { if ( serverMode !== ServerMode . Test ) { console . error ( error ) ; } let message = "Unexpected Server Error" ; if ( serverMode === ServerMode . Development ) { message += `\n\n${ String ( error ) }` ; } return new Response ( message , { status : 500 , headers : { "Content-Type" : "text/plain" } } ) ; } } } async function handleResourceRequest ( { loadContext , matches , request , serverMode } ) { let match = matches . slice ( - 1 ) [ 0 ] ; try { if ( isActionRequest ( request ) ) { return await callRouteAction ( { match , loadContext , request } ) ; } else { return await callRouteLoader ( { match , loadContext , request } ) ; } } catch ( error ) { if ( serverMode !== ServerMode . Test ) { console . error ( error ) ; } let message = "Unexpected Server Error" ; if ( serverMode === ServerMode . Development ) { message += `\n\n${ String ( error ) }` ; } return new Response ( message , { status : 500 , headers : { "Content-Type" : "text/plain" } } ) ; } } type RequestType = "data" | "document" | "resource" ; function getRequestType ( url , matches ) { if ( url . searchParams . has ( "_data" ) ) { return "data" ; } if ( ! matches ) { return "document" ; } let match = matches . slice ( - 1 ) [ 0 ] ; if ( ! match . route . module . default ) { return "resource" ; } return "document" ; } function isActionRequest ( request ) { let method = request . method . toLowerCase ( ) ; return ( method === "post" || method === "put" || method === "patch" || method === "delete" ) ; } function isHeadRequest ( request ) { return request . method . toLowerCase ( ) === "head" ; } function isValidRequestMethod ( request ) { return ( request . method . toLowerCase ( ) === "get" || isHeadRequest ( request ) || isActionRequest ( request ) ) ; } async function errorBoundaryError ( error , status ) { return json ( await serializeError ( error ) , { status , headers : { "X-Remix-Error" : "yes" } } ) ; } function isIndexRequestUrl ( url ) { let indexRequest = false ; for ( let param of url . searchParams . getAll ( "index" ) ) { if ( ! param ) { indexRequest = true ; } } return indexRequest ; } function getActionRequestMatch ( url , matches ) { let match = matches . slice ( - 1 ) [ 0 ] ; if ( ! isIndexRequestUrl ( url ) && match . route . id . endsWith ( "/index" ) ) { return matches . slice ( - 2 ) [ 0 ] ; } return match ; } function getDeepestRouteIdWithBoundary ( matches , key ) { let matched = getMatchesUpToDeepestBoundary ( matches , key ) . slice ( - 1 ) [ 0 ] ; return matched ? matched . route . id : null ; } function getMatchesUpToDeepestBoundary ( matches , key ) { let deepestBoundaryIndex = - 1 ; matches . forEach ( ( match , index ) => { if ( match . route . module [ key ] ) { deepestBoundaryIndex = index ; } } ) ; if ( deepestBoundaryIndex === - 1 ) { return [ ] ; } return matches . slice ( 0 , deepestBoundaryIndex + 1 ) ; } function getRenderableMatches ( matches , appState ) { if ( ! matches ) { return null ; } if ( ! appState . catch && ! appState . error ) { return matches ; } let lastRenderableIndex = - 1 ; matches . forEach ( ( match , index ) => { let id = match . route . id ; if ( appState . renderBoundaryRouteId === id || appState . loaderBoundaryRouteId === id || appState . catchBoundaryRouteId === id ) { lastRenderableIndex = index ; } } ) ; return matches . slice ( 0 , lastRenderableIndex + 1 ) ; }