All files / src handler.ts

100% Statements 37/37
100% Branches 12/12
100% Functions 9/9
100% Lines 37/37

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138                                                                                  162x       138x   138x 138x 138x   138x       24x       24x 24x 24x   24x     6x 138x     166x   166x 74x     24x   74x     166x 75x       75x     166x       166x   166x       1x       1x                     191x 191x 191x 190x 252x 92x 92x         98x         1x       166x 166x    
import {
  getScriptDownloadPath,
  getGetResultPath,
  WorkerEnv,
  getStatusPagePath,
  getIntegrationPathDepth,
  getIngressBaseHost,
  isScriptDownloadPathSet,
  isGetResultPathSet,
} from './env'
 
import { handleApiRequest, handleStatusPage } from './handlers'
import { getIngressEndpoint, createRoutePathPrefix, stripPrefixPathSegments, getAgentScriptEndpoint } from './utils'
 
export type Route = {
  /**
   * The route will match if request URL path after the integration
   * path is:
   * - `/${pathPrefix}`
   * - `/${pathPrefix}/.*`
   */
  pathPrefix: string
 
  /**
   * The request handler for the route
   *
   * @param request the {@link Request} the worker received
   * @param env the {@link WorkerEnv}
   * @param receivedRequestURL the {@link URL} for the received request
   * @param targetPath the URL path with the integration path removed
   * @returns the {@link Response} to return to the client
   */
  handler: (
    request: Request,
    env: WorkerEnv,
    receivedRequestURL: URL,
    targetPath: string
  ) => Response | Promise<Response>
}
 
function copySearchParams(oldURL: URL, newURL: URL): void {
  newURL.search = new URLSearchParams(oldURL.search).toString()
}
 
function createIngressRequestURL(env: WorkerEnv, receivedRequestURL: URL, targetPath: string) {
  const ingressBaseUrl = getIngressBaseHost(env)!
 
  const endpoint = getIngressEndpoint(ingressBaseUrl, receivedRequestURL.searchParams, targetPath)
  const newURL = new URL(endpoint)
  copySearchParams(receivedRequestURL, newURL)
 
  return newURL
}
 
function createAgentScriptURL(env: WorkerEnv, receivedRequestURL: URL) {
  const ingressBaseUrl = getIngressBaseHost(env)!
 
  // In V4, the Indentification API (Ingress) host also works as a proxy for the JavaScript agent CDN
  // that's why we are passing it here
  const agentScriptEndpoint = getAgentScriptEndpoint(ingressBaseUrl, receivedRequestURL.searchParams)
  const newURL = new URL(agentScriptEndpoint)
  copySearchParams(receivedRequestURL, newURL)
 
  return newURL
}
 
const DEFAULT_ROUTE: Route['handler'] = (request, env, receivedRequestURL, targetPath) =>
  handleApiRequest(request, env, createIngressRequestURL(env, receivedRequestURL, targetPath))
 
function createRoutes(env: WorkerEnv): Route[] {
  const routes: Route[] = []
 
  if (isScriptDownloadPathSet(env)) {
    const downloadScriptRoute: Route = {
      pathPrefix: createRoutePathPrefix(getScriptDownloadPath(env)),
      handler: (request, env, receivedRequestURL) =>
        handleApiRequest(request, env, createAgentScriptURL(env, receivedRequestURL)),
    }
    routes.push(downloadScriptRoute)
  }
 
  if (isGetResultPathSet(env)) {
    const ingressAPIRoute: Route = {
      pathPrefix: createRoutePathPrefix(getGetResultPath(env)),
      handler: DEFAULT_ROUTE,
    }
    routes.push(ingressAPIRoute)
  }
 
  const statusRoute: Route = {
    pathPrefix: createRoutePathPrefix(getStatusPagePath()),
    handler: handleStatusPage,
  }
  routes.push(statusRoute)
 
  return routes
}
 
function handleNoMatch(urlPathname: string): Response {
  const responseHeaders = new Headers({
    'content-type': 'application/json',
  })
 
  return new Response(JSON.stringify({ error: `unmatched path ${urlPathname}` }), {
    status: 404,
    headers: responseHeaders,
  })
}
 
export function handleRequestWithRoutes(
  request: Request,
  env: WorkerEnv,
  routes: Route[]
): Promise<Response> | Response {
  const url = new URL(request.url)
  const routeMatchingPath = stripPrefixPathSegments(url, getIntegrationPathDepth(env))
  if (routeMatchingPath) {
    for (const route of routes) {
      if (routeMatchingPath === route.pathPrefix || routeMatchingPath.startsWith(`${route.pathPrefix}/`)) {
        const targetPath = routeMatchingPath.slice(route.pathPrefix.length) || '/'
        return route.handler(request, env, url, targetPath)
      }
    }
 
    // If the request doesn't match any of the routes, handle it as an API request.
    return DEFAULT_ROUTE(request, env, url, routeMatchingPath)
  }
 
  // This should not occur in practice because the route patterns for
  // the worker are expected to prevent this case.
  return handleNoMatch(url.pathname)
}
 
export async function handleRequest(request: Request, env: WorkerEnv): Promise<Response> {
  const routes = createRoutes(env)
  return handleRequestWithRoutes(request, env, routes)
}