2023-04-16

1681617600
notes
 16 Apr 2023  notes

Rebuilding StatusCodes

The old implementation of StatusCodes was written in a hurry. To say the least, it was written in JavaScript (not TypeScript), had very clunky logic to read the URL, and opted to manipulate strings (within an array) in order to return a result. Feast your eyes on some deprecated code:

async function handleRequest(request) {
  const requestURL = new URL(request.url);

  // Isolate & sanitize the path
  const requestPath = requestURL.pathname.substring(1);

  const httpStatuses = [
    "100 Continue",
    "101 Switching Protocols",
    "102 Processing",
    "200 OK",
    "201 Created",
    "202 Accepted",
    "203 Non-authoritative Information",
    "204 No Content",
    "205 Reset Content",
    "206 Partial Content",
    "207 Multi-Status",
    "208 Already Reported",
    "226 IM Used",
    "300 Multiple Choices",
    "301 Moved Permanently",
    "302 Found",
    "303 See Other",
    "304 Not Modified",
    "305 Use Proxy",
    "307 Temporary Redirect",
    "308 Permanent Redirect",
    "400 Bad Request",
    "401 Unauthorized",
    "402 Payment Required",
    "403 Forbidden",
    "404 Not Found",
    "405 Method Not Allowed",
    "406 Not Acceptable",
    "407 Proxy Authentication Required",
    "408 Request Timeout",
    "409 Conflict",
    "410 Gone",
    "411 Length Required",
    "412 Precondition Failed",
    "413 Payload Too Large",
    "414 Request-URI Too Long",
    "415 Unsupported Media Type",
    "416 Requested Range Not Satisfiable",
    "417 Expectation Failed",
    "418 I'm a teapot",
    "421 Misdirected Request",
    "422 Unprocessable Entity",
    "423 Locked",
    "424 Failed Dependency",
    "426 Upgrade Required",
    "428 Precondition Required",
    "429 Too Many Requests",
    "431 Request Header Fields Too Large",
    "444 Connection Closed Without Response",
    "451 Unavailable For Legal Reasons",
    "499 Client Closed Request",
    "500 Internal Server Error",
    "501 Not Implemented",
    "502 Bad Gateway",
    "503 Service Unavailable",
    "504 Gateway Timeout",
    "505 HTTP Version Not Supported",
    "506 Variant Also Negotiates",
    "507 Insufficient Storage",
    "508 Loop Detected",
    "510 Not Extended",
    "511 Network Authentication Required",
    "599 Network Connect Timeout Error"
]

  // Filter array using the startsWith() method to find a matching HTTP Status Code
  const foundStatus = httpStatuses.filter(httpStatus => httpStatus.startsWith(requestPath));
  
  // Check if the pathname matches the validity of an HTTP Status Code
  if (requestPath.length === 3 && foundStatus.length > 0) {
    
    // Plaintext message for visual confirmation
    return new Response(`HTTP ${foundStatus}`,{
        
        // Return the actual three-digit matching HTTP Status Code
        status: `${requestPath}`,
    });
  } else if (requestPath.length === 0) {

    // Forward naked domain requests to info page
    return Response.redirect(`https://statuscodes.org/about`, 301);
  }
  else {
    // A plaintext error message, technically an Error 200 but here we are...
    return new Response("Unsupported HTTP Status Code");
}
}
addEventListener("fetch", async event => {
  event.respondWith(handleRequest(event.request))
})

Ouch! I decided to refactor it using Hono, a modern web framework built for the edge that has excellent TypeScript support, uses Web Standard APIs, and can deploy to Cloudflare Workers (among many other providers) out-of-the-box, lending itself to being an elegant and standardized choice.

Installing Hono

Intalling Hono is simple, a project can be bootstrapped with the following command:

npm create hono@latest my-app

You’ll be prompted for the runtime template, in this case I chose Cloudflare Workers:

? Which template do you want to use? » - Use arrow-keys. Return to submit.
    aws-lambda
    bun
    cloudflare-pages
>   cloudflare-workers
    deno
    fastly
    lagon
    nextjs
    nodejs

The dependencies aren’t installed just yet, so running npm i in the project folder will fix that.

The src/index.ts file will have some boilerplate code such as the Hono module import, an instance of Hono assigned to the app variable, and a default return for the root path:

import { Hono } from 'hono'

const app = new Hono()

app.get('/', (c) => c.text('Hello Hono!'))

export default app

The above is enough to get started, the command npm run dev will fire up the Wrangler dev server on localhost:8787

Bootstrapping Data

The first thing to do was to place the list of HTTP status codes into a separate file, so I stored them in an array (of tuples) which ensured it was safely typed and easily importable into the index.ts entrypoint:

// src/statusCodes.ts

export const statusCodes: Array<[number, string]> = [
  [ 100, "Continue" ],
  [ 101, "Switching Protocols" ],
  [ 102, "Processing" ],
  [ 200, "OK" ],
  [ 201, "Created" ],
  [ 202, "Accepted" ],
  [ 203, "Non-authoritative Information" ],
  [ 204, "No Content" ],
  [ 205, "Reset Content" ],
  [ 206, "Partial Content" ],
  [ 207, "Multi-Status" ],
  [ 208, "Already Reported" ],
  [ 226, "IM Used" ],
  [ 300, "Multiple Choices" ],
  [ 301, "Moved Permanently" ],
  [ 302, "Found" ],
  [ 303, "See Other" ],
  [ 304, "Not Modified" ],
  [ 305, "Use Proxy" ],
  [ 307, "Temporary Redirect" ],
  [ 308, "Permanent Redirect" ],
  [ 400, "Bad Request" ],
  [ 401, "Unauthorized" ],
  [ 402, "Payment Required" ],
  [ 403, "Forbidden" ],
  [ 404, "Not Found" ],
  [ 405, "Method Not Allowed" ],
  [ 406, "Not Acceptable" ],
  [ 407, "Proxy Authentication Required" ],
  [ 408, "Request Timeout" ],
  [ 409, "Conflict" ],
  [ 410, "Gone" ],
  [ 411, "Length Required" ],
  [ 412, "Precondition Failed" ],
  [ 413, "Payload Too Large" ],
  [ 414, "Request-URI Too Long" ],
  [ 415, "Unsupported Media Type" ],
  [ 416, "Requested Range Not Satisfiable" ],
  [ 417, "Expectation Failed" ],
  [ 418, "I'm a teapot" ],
  [ 421, "Misdirected Request" ],
  [ 422, "Unprocessable Entity" ],
  [ 423, "Locked" ],
  [ 424, "Failed Dependency" ],
  [ 426, "Upgrade Required" ],
  [ 428, "Precondition Required" ],
  [ 429, "Too Many Requests" ],
  [ 431, "Request Header Fields Too Large" ],
  [ 444, "Connection Closed Without Response" ],
  [ 451, "Unavailable For Legal Reasons" ],
  [ 499, "Client Closed Request" ],
  [ 500, "Internal Server Error" ],
  [ 501, "Not Implemented" ],
  [ 502, "Bad Gateway" ],
  [ 503, "Service Unavailable" ],
  [ 504, "Gateway Timeout" ],
  [ 505, "HTTP Version Not Supported" ],
  [ 506, "Variant Also Negotiates" ],
  [ 507, "Insufficient Storage" ],
  [ 508, "Loop Detected" ],
  [ 510, "Not Extended" ],
  [ 511, "Network Authentication Required" ],
  [ 599, "Network Connect Timeout Error" ]
]

It was then imported into the index.ts file:

import { statusCodes } from './statusCodes'

Adding Logic

The key to this entire project is the actual status code, so being able to lookup the code quickly and accurately is important. An array isn’t an ideal data structure to achieve this, therefore using a Map is better since the keys can be numbers, and lookups are very performant:

const allCodes: Map<number, string> = new Map(statusCodes)

TypeScript will infer the type from the Map keyword, but I like explicitly setting the type as a Map of tuples just to be thorough.

Hono offers an extensive but simple API when responding to GET requests from the Context object. The structure should be idiomatic and easy to understand, but you can read more about it in the docs: hono.dev/api/context#set-get

app.get('/:code', (c) => {
  /*
  ...
  */
})

Hono ingests the code URL parameter as a string, but I needed to parse this as an integer to be able to lookup the Map keys (which themselves are numbers):

let statusCode: number | any = parseInt(c.req.param('code'))

I used a number | any type union because TypeScript shows an error when returning just a number type in the Response object due to overloads. There’s most likely a proper a fix for this but laziness took over so here we are.

The actual logic then involves looking up the (parsed) URL parameter to see if a corresponding Map key exists, returning the value of that key as the actual HTTP response code, and also interpolating both the key and value as a text/plain response to be as helpful as possible:

if (allCodes.has(statusCode)) {
    c.header('Content-Type', 'text/plain')
    return c.text(`${statusCode} ${allCodes.get(statusCode)}`, statusCode)
  }
  return c.redirect('https://about.statuscodes.org', 302)

c.header is simply Hono’s method for adding headers to the Context object, which handles Request and Response. While I added statusCode to the return statement for brevity, it could also have been set via c.status(statusCode)

If the Map key doesn’t exist, then the URL parameter isn’t a valid status code, so regardless of the query the result should be a 302 redirect to the project’s about page:

return c.redirect('https://about.statuscodes.org', 302)

Similarly, any requests to the naked domain should result in a 301 permanent redirect to the project’s about page:

app.get('/', (c) => c.redirect('https://about.statuscodes.org', 301))

In both cases, Hono’s simplicity allows for creating an inline redirect using the c.redirect() Context method (with the 302 or 301 redirect codes passed as parameters).

Deploying Solution

The final Cloudflare Worker code looked like this:

// src/index.ts

import { Hono } from 'hono'

import { statusCodes } from './statusCodes'

const app = new Hono({ strict: false })

app.get('/:code', (c) => {
  const allCodes: Map<number, string> = new Map(statusCodes)
  // the URL param is a string, but the Map key is a number
  let statusCode: number | any = parseInt(c.req.param('code'))
  if (allCodes.has(statusCode)) {
    c.header('Content-Type', 'text/plain')
    return c.text(`${statusCode} ${allCodes.get(statusCode)}`, statusCode)
  }
  // 302 redirect since incorrect paths will be arbitrary
  return c.redirect('https://about.statuscodes.org', 302)
})

// 301 redirect for SEO when hitting the naked domain
app.get('/', (c) => c.redirect('https://about.statuscodes.org', 301))

export default app

Deploying was as simple as running npm run deploy and you can find the live project over at statuscodes.org

As I mentioned before, Hono is extremely performant and even more so when paired with Cloudflare’s edge network. Response times are generally below 2ms worldwide: check-host.net/check-ping?host=https://statuscodes.org/200

Copyright © Paramdeo Singh · All Rights Reserved · Built with Jekyll

This node last updated October 9, 2024 and is permanently morphing...

Paramdeo Singh Guyana

Generalist. Edgerunner. Riding the wave of consciousness in this treacherous mortal sea.

Technology Design Strategy Literature Personal Blogs
Search Site

Results are from Blog, Link Dumps, and #99Problems