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                                                                                  168x       141x   141x 141x 141x   141x       27x       27x 27x 27x   27x     7x 141x     172x   172x 77x     27x   77x     172x 78x       78x     172x       172x   172x       1x       1x                     197x 197x 197x 196x 258x 95x 95x         101x         1x       172x 172x    
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)
}