From 60b8da37445341ab96fa00780322de2c0fef045a Mon Sep 17 00:00:00 2001 From: Lucas Kostka Date: Mon, 31 Mar 2025 16:52:36 +0200 Subject: [PATCH 1/5] Added Hono version to Workers' examples --- .../docs/workers/examples/103-early-hints.mdx | 44 ++++ .../docs/workers/examples/ab-testing.mdx | 67 ++++++ .../accessing-the-cloudflare-object.mdx | 26 +++ .../workers/examples/aggregate-requests.mdx | 28 +++ .../docs/workers/examples/alter-headers.mdx | 41 ++++ .../workers/examples/auth-with-headers.mdx | 35 +++ .../docs/workers/examples/basic-auth.mdx | 104 +++++++++ .../docs/workers/examples/block-on-tls.mdx | 39 ++++ .../workers/examples/bulk-origin-proxy.mdx | 34 +++ .../docs/workers/examples/bulk-redirects.mdx | 40 ++++ .../docs/workers/examples/cache-api.mdx | 60 +++++ .../workers/examples/cache-post-request.mdx | 67 ++++++ .../docs/workers/examples/cache-tags.mdx | 50 ++++- .../workers/examples/cache-using-fetch.mdx | 72 ++++++ .../workers/examples/conditional-response.mdx | 87 ++++++++ .../workers/examples/cors-header-proxy.mdx | 143 ++++++++++++ .../examples/country-code-redirect.mdx | 44 ++++ .../docs/workers/examples/cron-trigger.mdx | 52 +++++ .../workers/examples/data-loss-prevention.mdx | 87 ++++++++ .../docs/workers/examples/debugging-logs.mdx | 73 ++++++ .../workers/examples/extract-cookie-value.mdx | 28 +++ .../docs/workers/examples/fetch-html.mdx | 20 ++ .../docs/workers/examples/fetch-json.mdx | 35 +++ .../examples/geolocation-app-weather.mdx | 83 +++++++ .../examples/geolocation-custom-styling.mdx | 195 ++++++++++++++++ .../examples/geolocation-hello-world.mdx | 72 ++++++ .../workers/examples/hot-link-protection.mdx | 40 ++++ .../docs/workers/examples/images-workers.mdx | 28 +++ .../docs/workers/examples/logging-headers.mdx | 31 +++ .../examples/modify-request-property.mdx | 60 +++++ .../docs/workers/examples/modify-response.mdx | 54 +++++ .../examples/multiple-cron-triggers.mdx | 44 ++++ .../workers/examples/openai-sdk-streaming.mdx | 53 +++++ .../docs/workers/examples/post-json.mdx | 60 +++++ .../protect-against-timing-attacks.mdx | 60 ++++- .../docs/workers/examples/read-post.mdx | 70 ++++++ .../docs/workers/examples/redirect.mdx | 38 ++++ .../examples/respond-with-another-site.mdx | 2 +- .../docs/workers/examples/return-html.mdx | 21 ++ .../docs/workers/examples/return-json.mdx | 18 ++ .../docs/workers/examples/rewrite-links.mdx | 52 +++++ .../workers/examples/security-headers.mdx | 104 +++++++++ .../workers/examples/signing-requests.mdx | 208 ++++++++++++++++++ .../examples/turnstile-html-rewriter.mdx | 114 ++++++++++ .../docs/workers/examples/websockets.mdx | 48 ++++ 45 files changed, 2723 insertions(+), 8 deletions(-) diff --git a/src/content/docs/workers/examples/103-early-hints.mdx b/src/content/docs/workers/examples/103-early-hints.mdx index adcae028ab00206..58cf58b4a8d3551 100644 --- a/src/content/docs/workers/examples/103-early-hints.mdx +++ b/src/content/docs/workers/examples/103-early-hints.mdx @@ -136,4 +136,48 @@ def on_fetch(request): return Response(HTML, headers=headers) ``` + + +```ts +import { Hono } from 'hono'; + +const app = new Hono(); + +const CSS = "body { color: red; }"; +const HTML = ` + + + + + Early Hints test + + + +

Early Hints test page

+ + +`; + +// Serve CSS file +app.get('/test.css', (c) => { + return c.body(CSS, { + headers: { + "content-type": "text/css", + }, + }); +}); + +// Serve HTML with early hints +app.get('*', (c) => { + return c.body(HTML, { + headers: { + "content-type": "text/html", + "link": "; rel=preload; as=style", + }, + }); +}); + +export default app; +``` +
diff --git a/src/content/docs/workers/examples/ab-testing.mdx b/src/content/docs/workers/examples/ab-testing.mdx index 3b71cca1afaeadf..3cba89d148c112c 100644 --- a/src/content/docs/workers/examples/ab-testing.mdx +++ b/src/content/docs/workers/examples/ab-testing.mdx @@ -137,4 +137,71 @@ async def on_fetch(request): return fetch(urlunparse(url)) ``` + + +```ts +import { Hono } from 'hono'; +import { getCookie, setCookie } from 'hono/cookie'; + +const app = new Hono(); + +const NAME = "myExampleWorkersABTest"; + +// Middleware to handle A/B testing logic +app.use('*', async (c) => { + const url = new URL(c.req.url); + + // Enable Passthrough to allow direct access to control and test routes + if (url.pathname.startsWith("/control") || url.pathname.startsWith("/test")) { + return fetch(c.req.raw); + } + + // Determine which group this requester is in + const abTestCookie = getCookie(c, NAME); + + if (abTestCookie === 'control') { + // User is in control group + url.pathname = "/control" + url.pathname; + return fetch(url); + } else if (abTestCookie === 'test') { + // User is in test group + url.pathname = "/test" + url.pathname; + return fetch(url); + } else { + // If there is no cookie, this is a new client + // Choose a group and set the cookie (50/50 split) + const group = Math.random() < 0.5 ? "test" : "control"; + + // Update URL path based on assigned group + if (group === "control") { + url.pathname = "/control" + url.pathname; + } else { + url.pathname = "/test" + url.pathname; + } + + // Fetch from origin with modified path + const res = await fetch(url); + + // Create a new response to avoid immutability issues + const newResponse = new Response(res.body, res); + + // Set cookie to enable persistent A/B sessions + setCookie(c, NAME, group, { + path: '/', + // Add additional cookie options as needed: + // secure: true, + // httpOnly: true, + // sameSite: 'strict', + }); + + // Copy the Set-Cookie header to the response + newResponse.headers.set('Set-Cookie', c.res.headers.get('Set-Cookie') || ''); + + return newResponse; + } +}); + +export default app; +``` + diff --git a/src/content/docs/workers/examples/accessing-the-cloudflare-object.mdx b/src/content/docs/workers/examples/accessing-the-cloudflare-object.mdx index 47039f0e893ec93..98c703c4c826acd 100644 --- a/src/content/docs/workers/examples/accessing-the-cloudflare-object.mdx +++ b/src/content/docs/workers/examples/accessing-the-cloudflare-object.mdx @@ -56,6 +56,32 @@ export default { } satisfies ExportedHandler; ``` + + +```ts +import { Hono } from "hono"; + +type Bindings = {}; + +const app = new Hono<{ Bindings: Bindings }>(); + +app.get("*", async (c) => { + // Access the raw request to get the cf object + const req = c.req.raw; + + // Check if the cf object is available + const data = + req.cf !== undefined + ? req.cf + : { error: "The `cf` object is not available inside the preview." }; + + // Return the data formatted with 2-space indentation + return c.json(data); +}); + +export default app; +``` + ```py diff --git a/src/content/docs/workers/examples/aggregate-requests.mdx b/src/content/docs/workers/examples/aggregate-requests.mdx index b3717a412a2fa2d..40aa95d9cdea3f4 100644 --- a/src/content/docs/workers/examples/aggregate-requests.mdx +++ b/src/content/docs/workers/examples/aggregate-requests.mdx @@ -58,6 +58,34 @@ export default { } satisfies ExportedHandler; ``` + + +```ts +import { Hono } from 'hono'; + +type Bindings = {}; + +const app = new Hono<{ Bindings: Bindings }>(); + +app.get('*', async (c) => { + // someHost is set up to return JSON responses + const someHost = "https://jsonplaceholder.typicode.com"; + const url1 = someHost + "/todos/1"; + const url2 = someHost + "/todos/2"; + + // Fetch both URLs concurrently + const responses = await Promise.all([fetch(url1), fetch(url2)]); + + // Parse JSON responses concurrently + const results = await Promise.all(responses.map(r => r.json())); + + // Return aggregated results + return c.json(results); +}); + +export default app; +``` + ```py diff --git a/src/content/docs/workers/examples/alter-headers.mdx b/src/content/docs/workers/examples/alter-headers.mdx index d85457e48c9ea9c..f04da40ccc49445 100644 --- a/src/content/docs/workers/examples/alter-headers.mdx +++ b/src/content/docs/workers/examples/alter-headers.mdx @@ -103,6 +103,47 @@ async def on_fetch(request): return Response(response.body, headers=new_headers) ``` + + +```ts +import { Hono } from 'hono'; + +const app = new Hono(); + +app.use('*', async (c, next) => { + // Process the request with the next middleware/handler + await next(); + + // After the response is generated, we can modify its headers + + // Add a custom header with a value + c.res.headers.append( + "x-workers-hello", + "Hello from Cloudflare Workers with Hono" + ); + + // Delete headers + c.res.headers.delete("x-header-to-delete"); + c.res.headers.delete("x-header2-to-delete"); + + // Adjust the value for an existing header + c.res.headers.set("x-header-to-change", "NewValue"); +}); + +app.get('*', async (c) => { + // Fetch content from example.com + const response = await fetch("https://example.com"); + + // Return the response body with original headers + // (our middleware will modify the headers before sending) + return new Response(response.body, { + headers: response.headers + }); +}); + +export default app; +``` + You can also use the [`custom-headers-example` template](https://github.com/kristianfreeman/custom-headers-example) to deploy this code to your custom domain. diff --git a/src/content/docs/workers/examples/auth-with-headers.mdx b/src/content/docs/workers/examples/auth-with-headers.mdx index 2c0cc9a0299df52..121f1ff57f84681 100644 --- a/src/content/docs/workers/examples/auth-with-headers.mdx +++ b/src/content/docs/workers/examples/auth-with-headers.mdx @@ -95,4 +95,39 @@ async def on_fetch(request): return Response("Sorry, you have supplied an invalid key.", status=403) ``` + + +```ts +import { Hono } from 'hono'; + +const app = new Hono(); + +// Add authentication middleware +app.use('*', async (c, next) => { + /** + * Define authentication constants + */ + const PRESHARED_AUTH_HEADER_KEY = "X-Custom-PSK"; + const PRESHARED_AUTH_HEADER_VALUE = "mypresharedkey"; + + // Get the pre-shared key from the request header + const psk = c.req.header(PRESHARED_AUTH_HEADER_KEY); + + if (psk === PRESHARED_AUTH_HEADER_VALUE) { + // Correct preshared header key supplied. Continue to the next handler. + await next(); + } else { + // Incorrect key supplied. Reject the request. + return c.text("Sorry, you have supplied an invalid key.", 403); + } +}); + +// Handle all authenticated requests by passing through to origin +app.all('*', async (c) => { + return fetch(c.req.raw); +}); + +export default app; +``` + diff --git a/src/content/docs/workers/examples/basic-auth.mdx b/src/content/docs/workers/examples/basic-auth.mdx index d175ce90b08546f..693cd5d171fe7d6 100644 --- a/src/content/docs/workers/examples/basic-auth.mdx +++ b/src/content/docs/workers/examples/basic-auth.mdx @@ -330,4 +330,108 @@ async fn fetch(req: Request, env: Env, _ctx: Context) -> Result { } } ``` + + +```ts +/** + * Shows how to restrict access using the HTTP Basic schema with Hono. + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication + * @see https://tools.ietf.org/html/rfc7617 + */ + +import { Hono } from 'hono'; +import { auth } from 'hono/basic-auth'; +import { Buffer } from "node:buffer"; + +// Define environment interface +interface Env { + Bindings: { + PASSWORD: string; + }; +} + +const app = new Hono(); + +// Helper function for comparing strings safely +const encoder = new TextEncoder(); +function timingSafeEqual(a: string, b: string) { + const aBytes = encoder.encode(a); + const bBytes = encoder.encode(b); + + if (aBytes.byteLength !== bBytes.byteLength) { + // Strings must be the same length in order to compare + // with crypto.subtle.timingSafeEqual + return false; + } + + return crypto.subtle.timingSafeEqual(aBytes, bBytes); +} + +// Public homepage - accessible to everyone +app.get('/', (c) => { + return c.text("Anyone can access the homepage."); +}); + +// Logout route +app.get('/logout', (c) => { + // Invalidate the "Authorization" header by returning a HTTP 401. + // We do not send a "WWW-Authenticate" header, as this would trigger + // a popup in the browser, immediately asking for credentials again. + return c.text("Logged out.", 401); +}); + +// Admin route - protected with Basic Auth +app.get('/admin', async (c) => { + const BASIC_USER = "admin"; + + // You will need an admin password. This should be + // attached to your Worker as an encrypted secret. + // Refer to https://developers.cloudflare.com/workers/configuration/secrets/ + const BASIC_PASS = c.env.PASSWORD ?? "password"; + + // The "Authorization" header is sent when authenticated + const authorization = c.req.header('Authorization'); + if (!authorization) { + // Prompts the user for credentials + return c.text("You need to login.", 401, { + 'WWW-Authenticate': 'Basic realm="my scope", charset="UTF-8"', + }); + } + + const [scheme, encoded] = authorization.split(" "); + + // The Authorization header must start with Basic, followed by a space + if (!encoded || scheme !== "Basic") { + return c.text("Malformed authorization header.", 400); + } + + const credentials = Buffer.from(encoded, "base64").toString(); + + // The username & password are split by the first colon + //=> example: "username:password" + const index = credentials.indexOf(":"); + const user = credentials.substring(0, index); + const pass = credentials.substring(index + 1); + + if (!timingSafeEqual(BASIC_USER, user) || !timingSafeEqual(BASIC_PASS, pass)) { + // Prompts the user for credentials again + return c.text("You need to login.", 401, { + 'WWW-Authenticate': 'Basic realm="my scope", charset="UTF-8"', + }); + } + + // Success! User is authenticated + return c.text("🎉 You have private access!", 200, { + 'Cache-Control': 'no-store', + }); +}); + +// Handle 404 for any other routes +app.notFound((c) => { + return c.text("Not Found.", 404); +}); + +export default app; +``` + \ No newline at end of file diff --git a/src/content/docs/workers/examples/block-on-tls.mdx b/src/content/docs/workers/examples/block-on-tls.mdx index 05741afacb2fb6c..5fd55f1c8f18214 100644 --- a/src/content/docs/workers/examples/block-on-tls.mdx +++ b/src/content/docs/workers/examples/block-on-tls.mdx @@ -69,6 +69,45 @@ export default { } satisfies ExportedHandler; ``` + + +```ts +import { Hono } from "hono"; + +type Bindings = {}; + +const app = new Hono<{ Bindings: Bindings }>(); + +// Middleware to check TLS version +app.use("*", async (c, next) => { + try { + // Access the raw request to get the cf object with TLS info + const request = c.req.raw; + const tlsVersion = request.cf?.tlsVersion; + + // Allow only TLS versions 1.2 and 1.3 + if (tlsVersion !== "TLSv1.2" && tlsVersion !== "TLSv1.3") { + return c.text("Please use TLS version 1.2 or higher.", 403); + } + + // Continue to the next handler if TLS version is acceptable + return next(); + } catch (err: any) { + // Handle errors (especially in preview mode where request.cf might not exist) + console.error( + "request.cf does not exist in the previewer, only in production", + ); + return c.text(`Error in workers script: ${err.message}`, 500); + } +}); + +app.get("/", async (c) => { + return c.text(`TLS Version: ${c.req.raw.cf.tlsVersion}`); +}); + +export default app; +``` + ```py diff --git a/src/content/docs/workers/examples/bulk-origin-proxy.mdx b/src/content/docs/workers/examples/bulk-origin-proxy.mdx index 28b7febfa193cdd..c5091e4a297e506 100644 --- a/src/content/docs/workers/examples/bulk-origin-proxy.mdx +++ b/src/content/docs/workers/examples/bulk-origin-proxy.mdx @@ -74,6 +74,40 @@ export default { } satisfies ExportedHandler; ``` + + +```ts +import { Hono } from 'hono'; + +type Bindings = {}; + +// An object with different URLs to fetch +const ORIGINS: Record = { + "starwarsapi.yourdomain.com": "swapi.dev", + "google.yourdomain.com": "www.google.com", +}; + +const app = new Hono<{ Bindings: Bindings }>(); + +app.all('*', async (c) => { + const url = new URL(c.req.url); + + // Check if incoming hostname is a key in the ORIGINS object + if (url.hostname in ORIGINS) { + const target = ORIGINS[url.hostname]; + url.hostname = target; + + // If it is, proxy request to that third party origin + return fetch(url.toString(), c.req.raw); + } + + // Otherwise, process request as normal + return fetch(c.req.raw); +}); + +export default app; +``` + ```py diff --git a/src/content/docs/workers/examples/bulk-redirects.mdx b/src/content/docs/workers/examples/bulk-redirects.mdx index e29389c3597dd73..9651a1937882f92 100644 --- a/src/content/docs/workers/examples/bulk-redirects.mdx +++ b/src/content/docs/workers/examples/bulk-redirects.mdx @@ -99,4 +99,44 @@ async def on_fetch(request): return fetch(request) ``` + + +```ts +import { Hono } from 'hono'; + +const app = new Hono(); + +// Configure your redirects +const externalHostname = "examples.cloudflareworkers.com"; + +const redirectMap = new Map([ + ["/bulk1", `https://${externalHostname}/redirect2`], + ["/bulk2", `https://${externalHostname}/redirect3`], + ["/bulk3", `https://${externalHostname}/redirect4`], + ["/bulk4", "https://google.com"], +]); + +// Middleware to handle redirects +app.use('*', async (c, next) => { + const path = new URL(c.req.url).pathname; + const location = redirectMap.get(path); + + if (location) { + // If path is in our redirect map, perform the redirect + return c.redirect(location, 301); + } + + // Otherwise, continue to the next handler + await next(); +}); + +// Default handler for requests that don't match any redirects +app.all('*', async (c) => { + // Pass through to origin + return fetch(c.req.raw); +}); + +export default app; +``` + diff --git a/src/content/docs/workers/examples/cache-api.mdx b/src/content/docs/workers/examples/cache-api.mdx index 4a80a1debf9bfe8..1b0e4b0866e3831 100644 --- a/src/content/docs/workers/examples/cache-api.mdx +++ b/src/content/docs/workers/examples/cache-api.mdx @@ -132,4 +132,64 @@ async def on_fetch(request, _env, ctx): return response ``` + + +```ts +import { Hono } from 'hono'; +import type { Context } from 'hono'; + +type Env = { + // Cloudflare Worker environment +}; + +const app = new Hono<{ Bindings: Env }>(); + +app.use('*', async (c, next) => { + const request = c.req.raw; + const cacheUrl = new URL(request.url); + + // Construct the cache key from the cache URL + const cacheKey = new Request(cacheUrl.toString(), request); + const cache = caches.default; + + // Check whether the value is already available in the cache + // if not, you will need to fetch it from origin, and store it in the cache + let response = await cache.match(cacheKey); + + if (!response) { + console.log( + `Response for request url: ${request.url} not present in cache. Fetching and caching request.` + ); + + // Continue to the next middleware to handle the request + await next(); + + // Get the response that was generated + response = new Response(c.res.body, { + status: c.res.status, + headers: c.res.headers, + }); + + // Cache API respects Cache-Control headers. Setting s-max-age to 10 + // will limit the response to be in cache for 10 seconds max + response.headers.append("Cache-Control", "s-maxage=10"); + + // Use waitUntil to capture the cache.put promise + c.executionCtx.waitUntil(cache.put(cacheKey, response.clone())); + + return response; + } else { + console.log(`Cache hit for: ${request.url}.`); + return response; + } +}); + +// Add a route to handle the request if it's not in cache +app.get('*', (c) => { + return c.text('Hello from Hono!'); +}); + +export default app; +``` + diff --git a/src/content/docs/workers/examples/cache-post-request.mdx b/src/content/docs/workers/examples/cache-post-request.mdx index 717cd0a73fec0ba..556b1088ce62761 100644 --- a/src/content/docs/workers/examples/cache-post-request.mdx +++ b/src/content/docs/workers/examples/cache-post-request.mdx @@ -147,4 +147,71 @@ async def on_fetch(request, _, ctx): return fetch(request) ``` + + +```ts +import { Hono } from 'hono'; + +interface Env {} + +const app = new Hono<{ Bindings: Env }>(); + +// Helper function to create SHA-256 hash +async function sha256(message: string): Promise { + // Encode as UTF-8 + const msgBuffer = await new TextEncoder().encode(message); + // Hash the message + const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer); + // Convert bytes to hex string + return [...new Uint8Array(hashBuffer)] + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); +} + +// Middleware for caching POST requests +app.post('*', async (c) => { + try { + // Get the request body + const body = await c.req.raw.clone().text(); + + // Hash the request body to use it as part of the cache key + const hash = await sha256(body); + + // Create the cache URL + const cacheUrl = new URL(c.req.url); + + // Store the URL in cache by prepending the body's hash + cacheUrl.pathname = "/posts" + cacheUrl.pathname + hash; + + // Convert to a GET to be able to cache + const cacheKey = new Request(cacheUrl.toString(), { + headers: c.req.raw.headers, + method: "GET", + }); + + const cache = caches.default; + + // Find the cache key in the cache + let response = await cache.match(cacheKey); + + // If not in cache, fetch response to POST request from origin + if (!response) { + response = await fetch(c.req.raw); + c.executionCtx.waitUntil(cache.put(cacheKey, response.clone())); + } + + return response; + } catch (e) { + return c.text("Error thrown " + e.message, 500); + } +}); + +// Handle all other HTTP methods +app.all('*', (c) => { + return fetch(c.req.raw); +}); + +export default app; +``` + diff --git a/src/content/docs/workers/examples/cache-tags.mdx b/src/content/docs/workers/examples/cache-tags.mdx index 64de2b5e5900d26..4b46e8b6d477e82 100644 --- a/src/content/docs/workers/examples/cache-tags.mdx +++ b/src/content/docs/workers/examples/cache-tags.mdx @@ -25,8 +25,7 @@ export default { const params = requestUrl.searchParams; const tags = params && params.has("tags") ? params.get("tags").split(",") : []; - const url = - params && params.has("uri") ? JSON.parse(params.get("uri")) : ""; + const url = params && params.has("uri") ? params.get("uri") : ""; if (!url) { const errorObject = { error: "URL cannot be empty", @@ -69,8 +68,7 @@ export default { const params = requestUrl.searchParams; const tags = params && params.has("tags") ? params.get("tags").split(",") : []; - const url = - params && params.has("uri") ? JSON.parse(params.get("uri")) : ""; + const url = params && params.has("uri") ? params.get("uri") : ""; if (!url) { const errorObject = { error: "URL cannot be empty", @@ -104,6 +102,50 @@ export default { } satisfies ExportedHandler; ``` + + +```ts +import { Hono } from "hono"; + +type Bindings = {}; + +const app = new Hono<{ Bindings: Bindings }>(); + +app.all("*", async (c) => { + try { + const url = new URL(c.req.url); + const params = url.searchParams; + const tags = params.has("tags") ? params.get("tags").split(",") : []; + const uri = params.has("uri") ? params.get("uri") : ""; + + if (!uri) { + return c.json({ error: "URL cannot be empty" }, 400); + } + + const init = { + cf: { + cacheTags: tags, + }, + }; + + const result = await fetch(uri, init); + const cacheStatus = result.headers.get("cf-cache-status"); + const lastModified = result.headers.get("last-modified"); + + const response = { + cache: cacheStatus, + lastModified: lastModified, + }; + + return c.json(response, result.status); + } catch (err: any) { + return c.json({ error: err.message }, 500); + } +}); + +export default app; +``` + ```py diff --git a/src/content/docs/workers/examples/cache-using-fetch.mdx b/src/content/docs/workers/examples/cache-using-fetch.mdx index 7cc76df5d6bf6b5..e42e86d9106b6d6 100644 --- a/src/content/docs/workers/examples/cache-using-fetch.mdx +++ b/src/content/docs/workers/examples/cache-using-fetch.mdx @@ -76,6 +76,46 @@ export default { } satisfies ExportedHandler; ``` + + +```ts +import { Hono } from 'hono'; + +type Bindings = {}; + +const app = new Hono<{ Bindings: Bindings }>(); + +app.all('*', async (c) => { + const url = new URL(c.req.url); + + // Only use the path for the cache key, removing query strings + // and always store using HTTPS, for example, https://www.example.com/file-uri-here + const someCustomKey = `https://${url.hostname}${url.pathname}`; + + // Fetch the request with custom cache settings + let response = await fetch(c.req.raw, { + cf: { + // Always cache this fetch regardless of content type + // for a max of 5 seconds before revalidating the resource + cacheTtl: 5, + cacheEverything: true, + // Enterprise only feature, see Cache API for other plans + cacheKey: someCustomKey, + }, + }); + + // Reconstruct the Response object to make its headers mutable + response = new Response(response.body, response); + + // Set cache control headers to cache on browser for 25 minutes + response.headers.set("Cache-Control", "max-age=1500"); + + return response; +}); + +export default app; +``` + ```py @@ -226,6 +266,38 @@ export default { } satisfies ExportedHandler; ``` + + +```ts +import { Hono } from 'hono'; + +type Bindings = {}; + +const app = new Hono<{ Bindings: Bindings }>(); + +app.all('*', async (c) => { + const originalUrl = c.req.url; + const url = new URL(originalUrl); + + // Randomly select a storage backend + if (Math.random() < 0.5) { + url.hostname = "example.s3.amazonaws.com"; + } else { + url.hostname = "example.storage.googleapis.com"; + } + + // Create a new request to the selected backend + const newRequest = new Request(url, c.req.raw); + + // Fetch using the original URL as the cache key + return fetch(newRequest, { + cf: { cacheKey: originalUrl }, + }); +}); + +export default app; +``` + Workers operating on behalf of different zones cannot affect each other's cache. You can only override cache keys when making requests within your own zone (in the above example `event.request.url` was the key stored), or requests to hosts that are not on Cloudflare. When making a request to another Cloudflare zone (for example, belonging to a different Cloudflare customer), that zone fully controls how its own content is cached within Cloudflare; you cannot override it. diff --git a/src/content/docs/workers/examples/conditional-response.mdx b/src/content/docs/workers/examples/conditional-response.mdx index 8ad70c4a9a371a5..74c76af5e3babd2 100644 --- a/src/content/docs/workers/examples/conditional-response.mdx +++ b/src/content/docs/workers/examples/conditional-response.mdx @@ -163,4 +163,91 @@ async def on_fetch(request): return fetch(request) ``` + + +```ts +import { Hono } from 'hono'; +import { HTTPException } from 'hono/http-exception'; + +// Define the RequestWithCf interface to add Cloudflare-specific properties +interface RequestWithCf extends Request { + cf?: { + asn?: number; + // Other CF properties can be added as needed + }; +} + +// Extend Hono's Environment interface to include our custom Request type +type Env = { + Variables: { + // Any custom variables you want to add to the context + }; +}; + +const app = new Hono(); + +// Middleware to handle all conditions before reaching the main handler +app.use('*', async (c, next) => { + const request = c.req.raw as RequestWithCf; + const BLOCKED_HOSTNAMES = ["nope.mywebsite.com", "bye.website.com"]; + + // Return a new Response based on a URL's hostname + const url = new URL(request.url); + if (BLOCKED_HOSTNAMES.includes(url.hostname)) { + return c.text("Blocked Host", 403); + } + + // Block paths ending in .doc or .xml based on the URL's file extension + const forbiddenExtRegExp = new RegExp(/\.(doc|xml)$/); + if (forbiddenExtRegExp.test(url.pathname)) { + return c.text("Blocked Extension", 403); + } + + // On User Agent + const userAgent = c.req.header("User-Agent") || ""; + if (userAgent.includes("bot")) { + return c.text("Block User Agent containing bot", 403); + } + + // On Client's IP address + const clientIP = c.req.header("CF-Connecting-IP"); + if (clientIP === "1.2.3.4") { + return c.text("Block the IP 1.2.3.4", 403); + } + + // On ASN + if (request.cf && request.cf.asn === 64512) { + return c.text("Block the ASN 64512 response"); + } + + // On Device Type + // Requires Enterprise "CF-Device-Type Header" zone setting or + // Page Rule with "Cache By Device Type" setting applied. + const device = c.req.header("CF-Device-Type"); + if (device === "mobile") { + return c.redirect("https://mobile.example.com"); + } + + // Continue to the next handler + await next(); +}); + +// Handle POST requests differently +app.post('*', (c) => { + return c.text("Response for POST"); +}); + +// Default handler for other methods +app.get('*', async (c) => { + console.error( + "Getting Client's IP address, device type, and ASN are not supported in playground. Must test on a live worker" + ); + + // Fetch the original request + return fetch(c.req.raw); +}); + +export default app; +``` + diff --git a/src/content/docs/workers/examples/cors-header-proxy.mdx b/src/content/docs/workers/examples/cors-header-proxy.mdx index f0db16e550324af..75ec9a7e2e7c95e 100644 --- a/src/content/docs/workers/examples/cors-header-proxy.mdx +++ b/src/content/docs/workers/examples/cors-header-proxy.mdx @@ -328,6 +328,149 @@ export default { } satisfies ExportedHandler; ``` + + +```ts +import { Hono } from 'hono'; +import { cors } from 'hono/cors'; + +type Bindings = {}; + +// The URL for the remote third party API you want to fetch from +// but does not implement CORS +const API_URL = "https://examples.cloudflareworkers.com/demos/demoapi"; + +// The endpoint you want the CORS reverse proxy to be on +const PROXY_ENDPOINT = "/corsproxy/"; + +const app = new Hono<{ Bindings: Bindings }>(); + +// Demo page handler +app.get('*', async (c) => { + const url = new URL(c.req.url); + + // Only handle non-proxy requests with this handler + if (url.pathname.startsWith(PROXY_ENDPOINT)) { + return c.next(); + } + + // Create the demo page HTML + const DEMO_PAGE = ` + + + +

API GET without CORS Proxy

+ Shows TypeError: Failed to fetch since CORS is misconfigured +

+ Waiting +

API GET with CORS Proxy

+

+ Waiting +

API POST with CORS Proxy + Preflight

+

+ Waiting + + + + `; + + return c.html(DEMO_PAGE); +}); + +// CORS proxy routes +app.on(['GET', 'HEAD', 'POST', 'OPTIONS'], PROXY_ENDPOINT + '*', async (c) => { + const url = new URL(c.req.url); + + // Handle OPTIONS preflight requests + if (c.req.method === 'OPTIONS') { + const origin = c.req.header('Origin'); + const requestMethod = c.req.header('Access-Control-Request-Method'); + const requestHeaders = c.req.header('Access-Control-Request-Headers'); + + if (origin && requestMethod && requestHeaders) { + // Handle CORS preflight requests + return new Response(null, { + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET,HEAD,POST,OPTIONS', + 'Access-Control-Max-Age': '86400', + 'Access-Control-Allow-Headers': requestHeaders, + }, + }); + } else { + // Handle standard OPTIONS request + return new Response(null, { + headers: { + 'Allow': 'GET, HEAD, POST, OPTIONS', + }, + }); + } + } + + // Handle actual requests + let apiUrl = url.searchParams.get('apiurl') || API_URL; + + // Rewrite request to point to API URL + const modifiedRequest = new Request(apiUrl, c.req.raw); + modifiedRequest.headers.set('Origin', new URL(apiUrl).origin); + + let response = await fetch(modifiedRequest); + + // Recreate the response so we can modify the headers + response = new Response(response.body, response); + + // Set CORS headers + response.headers.set('Access-Control-Allow-Origin', url.origin); + + // Append to/Add Vary header so browser will cache response correctly + response.headers.append('Vary', 'Origin'); + + return response; +}); + +// Handle method not allowed for proxy endpoint +app.all(PROXY_ENDPOINT + '*', (c) => { + return new Response(null, { + status: 405, + statusText: 'Method Not Allowed', + }); +}); + +export default app; +``` + ```py diff --git a/src/content/docs/workers/examples/country-code-redirect.mdx b/src/content/docs/workers/examples/country-code-redirect.mdx index 8f18795b5918064..fe6b7da2794c8f3 100644 --- a/src/content/docs/workers/examples/country-code-redirect.mdx +++ b/src/content/docs/workers/examples/country-code-redirect.mdx @@ -101,4 +101,48 @@ async def on_fetch(request): return fetch("https://example.com", request) ``` + + +```ts +import { Hono } from 'hono'; + +// Define the RequestWithCf interface to add Cloudflare-specific properties +interface RequestWithCf extends Request { + cf: { + country: string; + // Other CF properties can be added as needed + }; +} + +const app = new Hono(); + +app.get('*', async (c) => { + /** + * A map of the URLs to redirect to + */ + const countryMap: Record = { + US: "https://example.com/us", + EU: "https://example.com/eu", + }; + + // Cast the raw request to include Cloudflare-specific properties + const request = c.req.raw as RequestWithCf; + + // Use the cf object to obtain the country of the request + // more on the cf object: https://developers.cloudflare.com/workers/runtime-apis/request#incomingrequestcfproperties + const country = request.cf.country; + + if (country != null && country in countryMap) { + const url = countryMap[country]; + // Redirect using Hono's redirect helper + return c.redirect(url); + } else { + // Default fallback + return fetch("https://example.com", request); + } +}); + +export default app; +``` + diff --git a/src/content/docs/workers/examples/cron-trigger.mdx b/src/content/docs/workers/examples/cron-trigger.mdx index 87b5b10c67635a4..99084729e2d0355 100644 --- a/src/content/docs/workers/examples/cron-trigger.mdx +++ b/src/content/docs/workers/examples/cron-trigger.mdx @@ -17,6 +17,58 @@ import { Render, TabItem, Tabs, WranglerConfig } from "~/components"; + + +```ts +interface Env {} +export default { + async scheduled( + controller: ScheduledController, + env: Env, + ctx: ExecutionContext, + ) { + console.log("cron processed"); + }, +}; +``` + + + +```ts +import { Hono } from 'hono'; + +interface Env {} + +// Create Hono app +const app = new Hono<{ Bindings: Env }>(); + +// Regular routes for normal HTTP requests +app.get('/', (c) => c.text('Hello World!')); + +// Export both the app and a scheduled function +export default { + // The Hono app handles regular HTTP requests + fetch: app.fetch, + + // The scheduled function handles Cron triggers + async scheduled( + controller: ScheduledController, + env: Env, + ctx: ExecutionContext, + ) { + console.log("cron processed"); + + // You could also perform actions like: + // - Fetching data from external APIs + // - Updating KV or Durable Object storage + // - Running maintenance tasks + // - Sending notifications + }, +}; +``` + + + ## Set Cron Triggers in Wrangler Refer to [Cron Triggers](/workers/configuration/cron-triggers/) for more information on how to add a Cron Trigger. diff --git a/src/content/docs/workers/examples/data-loss-prevention.mdx b/src/content/docs/workers/examples/data-loss-prevention.mdx index f8536a08e044674..e7d6a3e1ac9e65d 100644 --- a/src/content/docs/workers/examples/data-loss-prevention.mdx +++ b/src/content/docs/workers/examples/data-loss-prevention.mdx @@ -219,4 +219,91 @@ async def on_fetch(request): return Response.new(text, response) ``` + + +```ts +import { Hono } from 'hono'; + +const app = new Hono(); + +// Configuration +const DEBUG = true; +const SOME_HOOK_SERVER = "https://webhook.flow-wolf.io/hook"; + +// Define sensitive data patterns +const sensitiveRegexsMap = { + creditCard: String.raw`\b(?:4[0-9]{12}(?:[0-9]{3})?|(?:5[1-5][0-9]{2}|222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|27[01][0-9]|2720)[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|6(?:011|5[0-9]{2})[0-9]{12}|(?:2131|1800|35\d{3})\d{11})\b`, + email: String.raw`\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b`, + phone: String.raw`\b07\d{9}\b`, +}; + +/** + * Alert a data breach by posting to a webhook server + */ +async function postDataBreach(request: Request) { + return await fetch(SOME_HOOK_SERVER, { + method: "POST", + headers: { + "content-type": "application/json;charset=UTF-8", + }, + body: JSON.stringify({ + ip: request.headers.get("cf-connecting-ip"), + time: Date.now(), + request: request, + }), + }); +} + +// Main middleware to handle data loss prevention +app.use('*', async (c) => { + // Fetch the origin response + const response = await fetch(c.req.raw); + + // Return origin response if response wasn't text + const contentType = response.headers.get("content-type") || ""; + if (!contentType.toLowerCase().includes("text/")) { + return response; + } + + // Get the response text + let text = await response.text(); + + // When debugging, replace the response from the origin with an email + text = DEBUG + ? text.replace("You may use this", "me@example.com may use this") + : text; + + // Check for sensitive data + for (const kind in sensitiveRegexsMap) { + const sensitiveRegex = new RegExp(sensitiveRegexsMap[kind], "ig"); + const match = sensitiveRegex.test(text); + + if (match) { + // Alert a data breach + await postDataBreach(c.req.raw); + + // Respond with a block if credit card, otherwise replace sensitive text with `*`s + if (kind === "creditCard") { + return c.text(`${kind} found\nForbidden\n`, 403); + } else { + return new Response(text.replace(sensitiveRegex, "**********"), { + status: response.status, + statusText: response.statusText, + headers: response.headers, + }); + } + } + } + + // Return the modified response + return new Response(text, { + status: response.status, + statusText: response.statusText, + headers: response.headers, + }); +}); + +export default app; +``` + diff --git a/src/content/docs/workers/examples/debugging-logs.mdx b/src/content/docs/workers/examples/debugging-logs.mdx index 0cee157c6f19e8d..d706124f7ba1225 100644 --- a/src/content/docs/workers/examples/debugging-logs.mdx +++ b/src/content/docs/workers/examples/debugging-logs.mdx @@ -142,4 +142,77 @@ async def on_fetch(request, _env, ctx): return response ``` + + +```ts +import { Hono } from 'hono'; + +// Define the environment with appropriate types +interface Env {} + +const app = new Hono<{ Bindings: Env }>(); + +// Service configured to receive logs +const LOG_URL = "https://log-service.example.com/"; + +// Function to post logs to an external service +async function postLog(data: string) { + return await fetch(LOG_URL, { + method: "POST", + body: data, + }); +} + +// Middleware to handle error logging +app.use('*', async (c, next) => { + try { + // Process the request with the next handler + await next(); + + // After processing, check if the response indicates an error + if (c.res && (!c.res.ok && !c.res.redirected)) { + const body = await c.res.clone().text(); + throw new Error( + "Bad response at origin. Status: " + + c.res.status + + " Body: " + + // Ensure the string is small enough to be a header + body.trim().substring(0, 10) + ); + } + + } catch (err) { + // Without waitUntil, the fetch to the logging service may not complete + c.executionCtx.waitUntil( + postLog(err.toString()) + ); + + // Get the error stack or error itself + const stack = JSON.stringify(err.stack) || err.toString(); + + // Create a new response with the error information + const response = c.res ? + new Response(stack, { + status: c.res.status, + headers: c.res.headers + }) : + new Response(stack, { status: 500 }); + + // Add debug headers + response.headers.set("X-Debug-stack", stack); + response.headers.set("X-Debug-err", err.toString()); + + // Set the modified response + c.res = response; + } +}); + +// Default route handler that passes requests through +app.all('*', async (c) => { + return fetch(c.req.raw); +}); + +export default app; +``` + diff --git a/src/content/docs/workers/examples/extract-cookie-value.mdx b/src/content/docs/workers/examples/extract-cookie-value.mdx index 035fcd8bc11a46f..e7ca1c827b34c34 100644 --- a/src/content/docs/workers/examples/extract-cookie-value.mdx +++ b/src/content/docs/workers/examples/extract-cookie-value.mdx @@ -73,10 +73,38 @@ async def on_fetch(request): return Response("No cookie with name: " + cookie_name) ``` + + +```ts +import { Hono } from 'hono'; +import { getCookie } from 'hono/cookie'; + +const app = new Hono(); + +app.get('*', (c) => { + // The name of the cookie + const COOKIE_NAME = "__uid"; + + // Get the specific cookie value using Hono's cookie helper + const cookieValue = getCookie(c, COOKIE_NAME); + + if (cookieValue) { + // Respond with the cookie value + return c.text(cookieValue); + } + + return c.text("No cookie with name: " + COOKIE_NAME); +}); + +export default app; +``` + :::note[External dependencies] This example requires the npm package [`cookie`](https://www.npmjs.com/package/cookie) to be installed in your JavaScript project. +The Hono example uses the built-in cookie utilities provided by Hono, so no external dependencies are needed for that implementation. + ::: diff --git a/src/content/docs/workers/examples/fetch-html.mdx b/src/content/docs/workers/examples/fetch-html.mdx index 4ba44fee72d32c3..cabe56f40fecd5f 100644 --- a/src/content/docs/workers/examples/fetch-html.mdx +++ b/src/content/docs/workers/examples/fetch-html.mdx @@ -49,4 +49,24 @@ async def on_fetch(request): return await fetch(remote, request) ``` + + +```ts +import { Hono } from 'hono'; + +const app = new Hono(); + +app.all('*', async (c) => { + /** + * Replace `remote` with the host you wish to send requests to + */ + const remote = "https://example.com"; + + // Forward the request to the remote server + return await fetch(remote, c.req.raw); +}); + +export default app; +``` + diff --git a/src/content/docs/workers/examples/fetch-json.mdx b/src/content/docs/workers/examples/fetch-json.mdx index eedeaf129abc3a0..2756ff24f62ab94 100644 --- a/src/content/docs/workers/examples/fetch-json.mdx +++ b/src/content/docs/workers/examples/fetch-json.mdx @@ -97,4 +97,39 @@ async def on_fetch(request): return Response(result, headers=headers) ``` + + +```ts +import { Hono } from 'hono'; + +type Env = {}; + +const app = new Hono<{ Bindings: Env }>(); + +app.get('*', async (c) => { + const url = "https://jsonplaceholder.typicode.com/todos/1"; + + // gatherResponse returns both content-type & response body as a string + async function gatherResponse(response: Response) { + const { headers } = response; + const contentType = headers.get("content-type") || ""; + + if (contentType.includes("application/json")) { + return { contentType, result: JSON.stringify(await response.json()) }; + } + + return { contentType, result: await response.text() }; + } + + const response = await fetch(url); + const { contentType, result } = await gatherResponse(response); + + return new Response(result, { + headers: { "content-type": contentType } + }); +}); + +export default app; +``` + diff --git a/src/content/docs/workers/examples/geolocation-app-weather.mdx b/src/content/docs/workers/examples/geolocation-app-weather.mdx index a3a65f984eb761e..ef190d1c3f5b68c 100644 --- a/src/content/docs/workers/examples/geolocation-app-weather.mdx +++ b/src/content/docs/workers/examples/geolocation-app-weather.mdx @@ -120,6 +120,89 @@ export default { } satisfies ExportedHandler; ``` + + +```ts +import { Hono } from 'hono'; +import { html } from 'hono/html'; + +type Bindings = {}; + +interface WeatherApiResponse { + data: { + aqi: number; + city: { + name: string; + url: string; + }; + iaqi: { + no2?: { v: number }; + o3?: { v: number }; + t?: { v: number }; + }; + }; +} + +const app = new Hono<{ Bindings: Bindings }>(); + +app.get('*', async (c) => { + // Get API endpoint + let endpoint = "https://api.waqi.info/feed/geo:"; + const token = ""; // Use a token from https://aqicn.org/api/ + + // Define styles + const html_style = `body{padding:6em; font-family: sans-serif;} h1{color:#f6821f}`; + + // Get geolocation from Cloudflare request + const req = c.req.raw; + const latitude = req.cf?.latitude; + const longitude = req.cf?.longitude; + + // Create complete API endpoint with coordinates + endpoint += `${latitude};${longitude}/?token=${token}`; + + // Fetch weather data + const init = { + headers: { + "content-type": "application/json;charset=UTF-8", + }, + }; + const response = await fetch(endpoint, init); + const content = await response.json() as WeatherApiResponse; + + // Build HTML content + const weatherContent = html` +

Weather 🌦

+

This is a demo using Workers geolocation data.

+

You are located at: ${latitude},${longitude}.

+

Based off sensor data from ${content.data.city.name}:

+

The AQI level is: ${content.data.aqi}.

+

The N02 level is: ${content.data.iaqi.no2?.v}.

+

The O3 level is: ${content.data.iaqi.o3?.v}.

+

The temperature is: ${content.data.iaqi.t?.v}°C.

+ `; + + // Complete HTML document + const htmlDocument = html` + + + Geolocation: Weather + + + +
+ ${weatherContent} +
+ + `; + + // Return HTML response + return c.html(htmlDocument); +}); + +export default app; +``` +
```py diff --git a/src/content/docs/workers/examples/geolocation-custom-styling.mdx b/src/content/docs/workers/examples/geolocation-custom-styling.mdx index f5f99c441cf0c33..416cd92c9f41102 100644 --- a/src/content/docs/workers/examples/geolocation-custom-styling.mdx +++ b/src/content/docs/workers/examples/geolocation-custom-styling.mdx @@ -355,4 +355,199 @@ export default { } satisfies ExportedHandler; ``` + + +```ts +import { Hono } from 'hono'; + +type Bindings = {}; +type ColorStop = { color: string; position: number }; + +const app = new Hono<{ Bindings: Bindings }>(); + +// Gradient configurations for each hour of the day (0-23) +const grads: ColorStop[][] = [ + [ + { color: "00000c", position: 0 }, + { color: "00000c", position: 0 }, + ], + [ + { color: "020111", position: 85 }, + { color: "191621", position: 100 }, + ], + [ + { color: "020111", position: 60 }, + { color: "20202c", position: 100 }, + ], + [ + { color: "020111", position: 10 }, + { color: "3a3a52", position: 100 }, + ], + [ + { color: "20202c", position: 0 }, + { color: "515175", position: 100 }, + ], + [ + { color: "40405c", position: 0 }, + { color: "6f71aa", position: 80 }, + { color: "8a76ab", position: 100 }, + ], + [ + { color: "4a4969", position: 0 }, + { color: "7072ab", position: 50 }, + { color: "cd82a0", position: 100 }, + ], + [ + { color: "757abf", position: 0 }, + { color: "8583be", position: 60 }, + { color: "eab0d1", position: 100 }, + ], + [ + { color: "82addb", position: 0 }, + { color: "ebb2b1", position: 100 }, + ], + [ + { color: "94c5f8", position: 1 }, + { color: "a6e6ff", position: 70 }, + { color: "b1b5ea", position: 100 }, + ], + [ + { color: "b7eaff", position: 0 }, + { color: "94dfff", position: 100 }, + ], + [ + { color: "9be2fe", position: 0 }, + { color: "67d1fb", position: 100 }, + ], + [ + { color: "90dffe", position: 0 }, + { color: "38a3d1", position: 100 }, + ], + [ + { color: "57c1eb", position: 0 }, + { color: "246fa8", position: 100 }, + ], + [ + { color: "2d91c2", position: 0 }, + { color: "1e528e", position: 100 }, + ], + [ + { color: "2473ab", position: 0 }, + { color: "1e528e", position: 70 }, + { color: "5b7983", position: 100 }, + ], + [ + { color: "1e528e", position: 0 }, + { color: "265889", position: 50 }, + { color: "9da671", position: 100 }, + ], + [ + { color: "1e528e", position: 0 }, + { color: "728a7c", position: 50 }, + { color: "e9ce5d", position: 100 }, + ], + [ + { color: "154277", position: 0 }, + { color: "576e71", position: 30 }, + { color: "e1c45e", position: 70 }, + { color: "b26339", position: 100 }, + ], + [ + { color: "163C52", position: 0 }, + { color: "4F4F47", position: 30 }, + { color: "C5752D", position: 60 }, + { color: "B7490F", position: 80 }, + { color: "2F1107", position: 100 }, + ], + [ + { color: "071B26", position: 0 }, + { color: "071B26", position: 30 }, + { color: "8A3B12", position: 80 }, + { color: "240E03", position: 100 }, + ], + [ + { color: "010A10", position: 30 }, + { color: "59230B", position: 80 }, + { color: "2F1107", position: 100 }, + ], + [ + { color: "090401", position: 50 }, + { color: "4B1D06", position: 100 }, + ], + [ + { color: "00000c", position: 80 }, + { color: "150800", position: 100 }, + ], +]; + +// Convert hour to CSS gradient +async function toCSSGradient(hour: number): Promise { + let css = "linear-gradient(to bottom,"; + const data = grads[hour]; + const len = data.length; + + for (let i = 0; i < len; i++) { + const item = data[i]; + css += ` #${item.color} ${item.position}%`; + if (i < len - 1) css += ","; + } + + return css + ")"; +} + +app.get('*', async (c) => { + const request = c.req.raw; + + // Base HTML style + let html_style = ` + html{width:100vw; height:100vh;} + body{padding:0; margin:0 !important;height:100%;} + #container { + display: flex; + flex-direction:column; + align-items: center; + justify-content: center; + height: 100%; + color:white; + font-family:sans-serif; + }`; + + // Get timezone from Cloudflare request + const timezone = request.cf?.timezone || 'UTC'; + console.log(timezone); + + // Get localized time + let localized_date = new Date( + new Date().toLocaleString("en-US", { timeZone: timezone }) + ); + + let hour = localized_date.getHours(); + let minutes = localized_date.getMinutes(); + + // Generate HTML content + let html_content = `

${hour}:${minutes}

`; + html_content += `

${timezone}

`; + + // Add background gradient based on hour + html_style += `body{background:${await toCSSGradient(hour)};}`; + + // Complete HTML document + let html = ` + + + Geolocation: Customized Design + + + +
+ ${html_content} +
+ `; + + return c.html(html); +}); + +export default app; +``` +
diff --git a/src/content/docs/workers/examples/geolocation-hello-world.mdx b/src/content/docs/workers/examples/geolocation-hello-world.mdx index 1c8f7f52e200b97..aed295bc3badae0 100644 --- a/src/content/docs/workers/examples/geolocation-hello-world.mdx +++ b/src/content/docs/workers/examples/geolocation-hello-world.mdx @@ -137,4 +137,76 @@ async def on_fetch(request): return Response(html, headers=headers) ``` + + +```ts +import { Hono } from "hono"; +import { html } from "hono/html"; + +// Define the RequestWithCf interface to add Cloudflare-specific properties +interface RequestWithCf extends Request { + cf: { + // Cloudflare-specific properties for geolocation + colo: string; + country: string; + city: string; + continent: string; + latitude: string; + longitude: string; + postalCode: string; + metroCode: string; + region: string; + regionCode: string; + timezone: string; + // Add other CF properties as needed + }; +} + +const app = new Hono(); + +app.get("*", (c) => { + // Cast the raw request to include Cloudflare-specific properties + const request = c.req.raw as RequestWithCf; + + // Define styles + const html_style = + "body{padding:6em; font-family: sans-serif;} h1{color:#f6821f;}"; + + // Create content with geolocation data + let html_content = html`

Colo: ${request.cf.colo}

+

Country: ${request.cf.country}

+

City: ${request.cf.city}

+

Continent: ${request.cf.continent}

+

Latitude: ${request.cf.latitude}

+

Longitude: ${request.cf.longitude}

+

PostalCode: ${request.cf.postalCode}

+

MetroCode: ${request.cf.metroCode}

+

Region: ${request.cf.region}

+

RegionCode: ${request.cf.regionCode}

+

Timezone: ${request.cf.timezone}

`; + + // Compose the full HTML + const htmlContent = html` + + Geolocation: Hello World + + + +

Geolocation: Hello World!

+

+ You now have access to geolocation data about where your user is + visiting from. +

+ ${html_content} + `; + + // Return the HTML response + return c.html(htmlContent); +}); + +export default app; +``` +
diff --git a/src/content/docs/workers/examples/hot-link-protection.mdx b/src/content/docs/workers/examples/hot-link-protection.mdx index 0555823f2f251cb..215019617c479d8 100644 --- a/src/content/docs/workers/examples/hot-link-protection.mdx +++ b/src/content/docs/workers/examples/hot-link-protection.mdx @@ -106,4 +106,44 @@ async def on_fetch(request): return response ``` + + +```ts +import { Hono } from 'hono'; + +const app = new Hono(); + +// Middleware for hot-link protection +app.use('*', async (c, next) => { + const HOMEPAGE_URL = "https://tutorial.cloudflareworkers.com/"; + const PROTECTED_TYPE = "image/"; + + // Continue to the next handler to get the response + await next(); + + // If we have a response, check for hotlinking + if (c.res) { + // If it's an image, engage hotlink protection based on the Referer header + const referer = c.req.header("Referer"); + const contentType = c.res.headers.get("Content-Type") || ""; + + if (referer && contentType.startsWith(PROTECTED_TYPE)) { + // If the hostnames don't match, it's a hotlink + if (new URL(referer).hostname !== new URL(c.req.url).hostname) { + // Redirect the user to your website + c.res = c.redirect(HOMEPAGE_URL, 302); + } + } + } +}); + +// Default route handler that passes through the request to the origin +app.all('*', async (c) => { + // Fetch the original request + return fetch(c.req.raw); +}); + +export default app; +``` + diff --git a/src/content/docs/workers/examples/images-workers.mdx b/src/content/docs/workers/examples/images-workers.mdx index eaf8ade72b7ceea..776068d2a938b98 100644 --- a/src/content/docs/workers/examples/images-workers.mdx +++ b/src/content/docs/workers/examples/images-workers.mdx @@ -59,6 +59,34 @@ export default { } satisfies ExportedHandler; ``` + + +```ts +import { Hono } from 'hono'; + +interface Env { + // You can store your account hash as a binding variable + ACCOUNT_HASH?: string; +} + +const app = new Hono<{ Bindings: Env }>(); + +app.get('*', async (c) => { + // You can find this in the dashboard, it should look something like this: ZWd9g1K7eljCn_KDTu_MWA + // Either get it from environment or hardcode it here + const accountHash = c.env.ACCOUNT_HASH || ""; + + const url = new URL(c.req.url); + + // A request to something like cdn.example.com/83eb7b2-5392-4565-b69e-aff66acddd00/public + // will fetch "https://imagedelivery.net//83eb7b2-5392-4565-b69e-aff66acddd00/public" + + return fetch(`https://imagedelivery.net/${accountHash}${url.pathname}`); +}); + +export default app; +``` + ```py diff --git a/src/content/docs/workers/examples/logging-headers.mdx b/src/content/docs/workers/examples/logging-headers.mdx index cd388f0336e16db..ab634ea220660ed 100644 --- a/src/content/docs/workers/examples/logging-headers.mdx +++ b/src/content/docs/workers/examples/logging-headers.mdx @@ -62,6 +62,37 @@ async fn fetch(req: HttpRequest, _env: Env, _ctx: Context) -> Result { Response::ok("hello world") } ``` + + +```ts +import { Hono } from 'hono'; + +const app = new Hono(); + +app.get('*', (c) => { + // Different ways to log headers in Hono: + + // 1. Using Map to display headers in console + console.log('Headers as Map:', new Map(c.req.raw.headers)); + + // 2. Using spread operator to log headers + console.log('Headers spread:', [...c.req.raw.headers]); + + // 3. Using Object.fromEntries to convert to an object + console.log('Headers as Object:', Object.fromEntries(c.req.raw.headers)); + + // 4. Hono's built-in header accessor (for individual headers) + console.log('User-Agent:', c.req.header('User-Agent')); + + // 5. Using c.req.headers to get all headers + console.log('All headers from Hono context:', c.req.headers); + + return c.text('Hello world'); +}); + +export default app; +``` + --- diff --git a/src/content/docs/workers/examples/modify-request-property.mdx b/src/content/docs/workers/examples/modify-request-property.mdx index 0bf3856c4615d47..74f08e72a9dfe7f 100644 --- a/src/content/docs/workers/examples/modify-request-property.mdx +++ b/src/content/docs/workers/examples/modify-request-property.mdx @@ -184,4 +184,64 @@ async def on_fetch(request): return Response.new({"error": str(e)}, status=500) ``` + + +```ts +import { Hono } from 'hono'; + +const app = new Hono(); + +app.all('*', async (c) => { + /** + * Example someHost is set up to return raw JSON + */ + const someHost = "example.com"; + const someUrl = "https://foo.example.com/api.js"; + + try { + // Create a URL object to modify the hostname + const url = new URL(someUrl); + url.hostname = someHost; + + // In Hono, we can create a new request with modified properties + + // Define the new request properties + const newRequestInit = { + // Change method + method: "POST", + // Change body + body: JSON.stringify({ bar: "foo" }), + // Change the redirect mode + redirect: "follow" as RequestRedirect, + // Change headers, note this method will erase existing headers + headers: { + "Content-Type": "application/json", + "X-Example": "bar" + }, + // Change a Cloudflare feature on the outbound response + cf: { apps: false }, + }; + + // Create a new request + // First create a clone of the original request with the new properties + const requestClone = new Request(c.req.raw, newRequestInit); + + // Then create a new request with the modified URL + const newRequest = new Request(url.toString(), requestClone); + + // Send the modified request + const response = await fetch(newRequest); + + // Return the response + return response; + + } catch (e) { + // Handle errors + return c.json({ error: e.message }, 500); + } +}); + +export default app; +``` + diff --git a/src/content/docs/workers/examples/modify-response.mdx b/src/content/docs/workers/examples/modify-response.mdx index cbc4f80b8d7daaa..5ecc7de99f8021f 100644 --- a/src/content/docs/workers/examples/modify-response.mdx +++ b/src/content/docs/workers/examples/modify-response.mdx @@ -153,4 +153,58 @@ async def on_fetch(request): return response ``` + + +```ts +import { Hono } from 'hono'; + +const app = new Hono(); + +app.get('*', async (c) => { + /** + * Header configuration + */ + const headerNameSrc = "foo"; // Header to get the new value from + const headerNameDst = "Last-Modified"; // Header to set based off of value in src + + /** + * Response properties are immutable. With Hono, we can modify the response + * by creating custom response objects. + */ + const originalResponse = await fetch(c.req.raw); + + // Get the JSON body from the original response + const originalBody = await originalResponse.json(); + + // Modify the body by adding a new property + const modifiedBody = { + foo: "bar", + ...originalBody + }; + + // Create a new custom response with modified status, headers, and body + const response = new Response(JSON.stringify(modifiedBody), { + status: 500, + statusText: "some message", + headers: originalResponse.headers, + }); + + // Add a header using set method + response.headers.set("foo", "bar"); + + // Set destination header to the value of the source header + const src = response.headers.get(headerNameSrc); + if (src != null) { + response.headers.set(headerNameDst, src); + console.log( + `Response header "${headerNameDst}" was set to "${response.headers.get(headerNameDst)}"` + ); + } + + return response; +}); + +export default app; +``` + diff --git a/src/content/docs/workers/examples/multiple-cron-triggers.mdx b/src/content/docs/workers/examples/multiple-cron-triggers.mdx index c32914f9d16ebcf..e2e9ed888bf4050 100644 --- a/src/content/docs/workers/examples/multiple-cron-triggers.mdx +++ b/src/content/docs/workers/examples/multiple-cron-triggers.mdx @@ -70,6 +70,50 @@ export default { }; ``` + + +```ts +import { Hono } from "hono"; + +interface Env {} + +// Create Hono app +const app = new Hono<{ Bindings: Env }>(); + +// Regular routes for normal HTTP requests +app.get("/", (c) => c.text("Multiple Cron Trigger Example")); + +// Export both the app and a scheduled function +export default { + // The Hono app handles regular HTTP requests + fetch: app.fetch, + + // The scheduled function handles Cron triggers + async scheduled( + controller: ScheduledController, + env: Env, + ctx: ExecutionContext, + ) { + // Check which cron schedule triggered this execution + switch (controller.cron) { + case "*/3 * * * *": + // Every three minutes + await updateAPI(); + break; + case "*/10 * * * *": + // Every ten minutes + await updateAPI2(); + break; + case "*/45 * * * *": + // Every forty-five minutes + await updateAPI3(); + break; + } + console.log("cron processed"); + }, +}; +``` + ## Test Cron Triggers using Wrangler diff --git a/src/content/docs/workers/examples/openai-sdk-streaming.mdx b/src/content/docs/workers/examples/openai-sdk-streaming.mdx index c9c416a30fe5fc7..2a3df112afbfe2f 100644 --- a/src/content/docs/workers/examples/openai-sdk-streaming.mdx +++ b/src/content/docs/workers/examples/openai-sdk-streaming.mdx @@ -14,6 +14,8 @@ head: [] description: Use the OpenAI v4 SDK to stream responses from OpenAI. --- +import { TabItem, Tabs } from "~/components"; + In order to run this code, you must install the OpenAI SDK by running `npm i openai`. :::note @@ -22,6 +24,8 @@ For analytics, caching, rate limiting, and more, you can also send requests like ::: + + ```ts import OpenAI from "openai"; @@ -59,3 +63,52 @@ export default { }, } satisfies ExportedHandler; ``` + + + +```ts +import { Hono } from 'hono'; +import OpenAI from 'openai'; + +interface Env { + OPENAI_API_KEY: string; +} + +const app = new Hono<{ Bindings: Env }>(); + +app.get('*', async (c) => { + const openai = new OpenAI({ + apiKey: c.env.OPENAI_API_KEY, + }); + + // Create a TransformStream to handle streaming data + let { readable, writable } = new TransformStream(); + let writer = writable.getWriter(); + const textEncoder = new TextEncoder(); + + c.executionCtx.waitUntil( + (async () => { + const stream = await openai.chat.completions.create({ + model: "gpt-4o-mini", + messages: [{ role: "user", content: "Tell me a story" }], + stream: true, + }); + + // Loop over the data as it is streamed and write to the writeable + for await (const part of stream) { + writer.write( + textEncoder.encode(part.choices[0]?.delta?.content || ""), + ); + } + writer.close(); + })() + ); + + // Send the readable back to the browser + return new Response(readable); +}); + +export default app; +``` + + diff --git a/src/content/docs/workers/examples/post-json.mdx b/src/content/docs/workers/examples/post-json.mdx index 14227c2ef854655..f088344d09b1e4f 100644 --- a/src/content/docs/workers/examples/post-json.mdx +++ b/src/content/docs/workers/examples/post-json.mdx @@ -163,4 +163,64 @@ async def on_fetch(_request): return Response.new(result, headers=headers) ``` + + +```ts +import { Hono } from 'hono'; + +const app = new Hono(); + +app.get('*', async (c) => { + /** + * Example someHost is set up to take in a JSON request + * Replace url with the host you wish to send requests to + */ + const someHost = "https://examples.cloudflareworkers.com/demos"; + const url = someHost + "/requests/json"; + const body = { + results: ["default data to send"], + errors: null, + msg: "I sent this to the fetch", + }; + + /** + * gatherResponse awaits and returns a response body as a string. + * Use await gatherResponse(..) in an async function to get the response body + */ + async function gatherResponse(response: Response) { + const { headers } = response; + const contentType = headers.get("content-type") || ""; + + if (contentType.includes("application/json")) { + return { contentType, result: JSON.stringify(await response.json()) }; + } else if (contentType.includes("application/text")) { + return { contentType, result: await response.text() }; + } else if (contentType.includes("text/html")) { + return { contentType, result: await response.text() }; + } else { + return { contentType, result: await response.text() }; + } + } + + const init = { + body: JSON.stringify(body), + method: "POST", + headers: { + "content-type": "application/json;charset=UTF-8", + }, + }; + + const response = await fetch(url, init); + const { contentType, result } = await gatherResponse(response); + + return new Response(result, { + headers: { + "content-type": contentType, + }, + }); +}); + +export default app; +``` + diff --git a/src/content/docs/workers/examples/protect-against-timing-attacks.mdx b/src/content/docs/workers/examples/protect-against-timing-attacks.mdx index 81650f8510e5673..805f7240327472e 100644 --- a/src/content/docs/workers/examples/protect-against-timing-attacks.mdx +++ b/src/content/docs/workers/examples/protect-against-timing-attacks.mdx @@ -81,17 +81,71 @@ async def on_fetch(request, env): if len(auth_token) != len(secret): return Response("Unauthorized", status=401) - if a.byteLength != b.byteLength: - return Response("Unauthorized", status=401) - encoder = TextEncoder.new() a = encoder.encode(auth_token) b = encoder.encode(secret) + if a.byteLength != b.byteLength: + return Response("Unauthorized", status=401) + if not crypto.subtle.timingSafeEqual(a, b): return Response("Unauthorized", status=401) return Response("Welcome!") ``` + + +```ts +import { Hono } from 'hono'; + +interface Environment { + Bindings: { + MY_SECRET_VALUE?: string; + } +} + +const app = new Hono(); + +// Middleware to handle authentication with timing-safe comparison +app.use('*', async (c, next) => { + const secret = c.env.MY_SECRET_VALUE; + + if (!secret) { + return c.text("Missing secret binding", 500); + } + + const authToken = c.req.header("Authorization") || ""; + + // Early length check to avoid unnecessary processing + if (authToken.length !== secret.length) { + return c.text("Unauthorized", 401); + } + + const encoder = new TextEncoder(); + + const a = encoder.encode(authToken); + const b = encoder.encode(secret); + + if (a.byteLength !== b.byteLength) { + return c.text("Unauthorized", 401); + } + + // Perform timing-safe comparison + if (!crypto.subtle.timingSafeEqual(a, b)) { + return c.text("Unauthorized", 401); + } + + // If we got here, the auth token is valid + await next(); +}); + +// Protected route +app.get('*', (c) => { + return c.text("Welcome!"); +}); + +export default app; +``` + diff --git a/src/content/docs/workers/examples/read-post.mdx b/src/content/docs/workers/examples/read-post.mdx index df1f62d75c2214b..78742f50535721f 100644 --- a/src/content/docs/workers/examples/read-post.mdx +++ b/src/content/docs/workers/examples/read-post.mdx @@ -214,4 +214,74 @@ async fn fetch(req: Request, _env: Env, _ctx: Context) -> Result { } ``` + + +```ts +import { Hono } from "hono"; +import { html } from "hono/html"; + +const app = new Hono(); + +/** + * readRequestBody reads in the incoming request body + * @param {Request} request the incoming request to read from + */ +async function readRequestBody(request: Request): Promise { + const contentType = request.headers.get("content-type") || ""; + + if (contentType.includes("application/json")) { + const body = await request.json(); + return JSON.stringify(body); + } else if (contentType.includes("application/text")) { + return request.text(); + } else if (contentType.includes("text/html")) { + return request.text(); + } else if (contentType.includes("form")) { + const formData = await request.formData(); + const body: Record = {}; + for (const [key, value] of formData.entries()) { + body[key] = value.toString(); + } + return JSON.stringify(body); + } else { + // Perhaps some other type of data was submitted in the form + // like an image, or some other binary data. + return "a file"; + } +} + +const someForm = html` + + +
+
+ + +
+
+ +
+
+ + `; + +app.get("*", async (c) => { + const url = c.req.url; + + if (url.includes("form")) { + return c.html(someForm); + } + + return c.text("The request was a GET"); +}); + +app.post("*", async (c) => { + const reqBody = await readRequestBody(c.req.raw); + const retBody = `The request body sent in was ${reqBody}`; + return c.text(retBody); +}); + +export default app; +``` +
diff --git a/src/content/docs/workers/examples/redirect.mdx b/src/content/docs/workers/examples/redirect.mdx index 0baffd289f2aaa7..d217581ccc2243f 100644 --- a/src/content/docs/workers/examples/redirect.mdx +++ b/src/content/docs/workers/examples/redirect.mdx @@ -65,6 +65,22 @@ async fn fetch(_req: Request, _env: Env, _ctx: Context) -> Result { ``` + + +```ts +import { Hono } from 'hono'; + +const app = new Hono(); + +app.all('*', (c) => { + const destinationURL = "https://example.com"; + const statusCode = 301; + return c.redirect(destinationURL, statusCode); +}); + +export default app; +``` + ## Redirect requests from one domain to another @@ -147,4 +163,26 @@ async fn fetch(req: Request, _env: Env, _ctx: Context) -> Result { ``` + + +```ts +import { Hono } from 'hono'; + +const app = new Hono(); + +app.all('*', (c) => { + const base = "https://example.com"; + const statusCode = 301; + + const { pathname, search } = new URL(c.req.url); + + const destinationURL = `${base}${pathname}${search}`; + console.log(destinationURL); + + return c.redirect(destinationURL, statusCode); +}); + +export default app; +``` + diff --git a/src/content/docs/workers/examples/respond-with-another-site.mdx b/src/content/docs/workers/examples/respond-with-another-site.mdx index a6b0387439a11cf..432155e08ddc8c7 100644 --- a/src/content/docs/workers/examples/respond-with-another-site.mdx +++ b/src/content/docs/workers/examples/respond-with-another-site.mdx @@ -63,4 +63,4 @@ def on_fetch(request): return fetch("https://example.com") ``` - + diff --git a/src/content/docs/workers/examples/return-html.mdx b/src/content/docs/workers/examples/return-html.mdx index 9a610662a23782e..09a0caa5dc3c080 100644 --- a/src/content/docs/workers/examples/return-html.mdx +++ b/src/content/docs/workers/examples/return-html.mdx @@ -77,6 +77,27 @@ async fn fetch(_req: Request, _env: Env, _ctx: Context) -> Result { } ``` + + +```ts +import { Hono } from "hono"; +import { html } from "hono/html"; + +const app = new Hono(); + +app.get("*", (c) => { + const doc = html` + +

Hello World

+

This markup was generated by a Cloudflare Worker with Hono.

+ `; + + return c.html(doc); +}); + +export default app; +``` +
diff --git a/src/content/docs/workers/examples/return-json.mdx b/src/content/docs/workers/examples/return-json.mdx index 9cf9dfa612db7f6..2c791d520c67bd9 100644 --- a/src/content/docs/workers/examples/return-json.mdx +++ b/src/content/docs/workers/examples/return-json.mdx @@ -73,4 +73,22 @@ async fn fetch(_req: Request, _env: Env, _ctx: Context) -> Result { } ``` + + +```ts +import { Hono } from 'hono'; + +const app = new Hono(); + +app.get('*', (c) => { + const data = { + hello: "world", + }; + + return c.json(data); +}); + +export default app; +``` + diff --git a/src/content/docs/workers/examples/rewrite-links.mdx b/src/content/docs/workers/examples/rewrite-links.mdx index a68baada4ed9531..ceccb897d533a2d 100644 --- a/src/content/docs/workers/examples/rewrite-links.mdx +++ b/src/content/docs/workers/examples/rewrite-links.mdx @@ -129,4 +129,56 @@ async def on_fetch(request): return res ``` + + +```ts +import { Hono } from 'hono'; +import { html } from 'hono/html'; + +const app = new Hono(); + +app.get('*', async (c) => { + const OLD_URL = "developer.mozilla.org"; + const NEW_URL = "mynewdomain.com"; + + class AttributeRewriter { + attributeName: string; + + constructor(attributeName: string) { + this.attributeName = attributeName; + } + + element(element: Element) { + const attribute = element.getAttribute(this.attributeName); + if (attribute) { + element.setAttribute( + this.attributeName, + attribute.replace(OLD_URL, NEW_URL) + ); + } + } + } + + // Make a fetch request using the original request + const res = await fetch(c.req.raw); + const contentType = res.headers.get("Content-Type") || ""; + + // If the response is HTML, transform it with HTMLRewriter + if (contentType.startsWith("text/html")) { + const rewriter = new HTMLRewriter() + .on("a", new AttributeRewriter("href")) + .on("img", new AttributeRewriter("src")); + + return new Response(rewriter.transform(res).body, { + headers: res.headers + }); + } else { + // Pass through the response as is + return res; + } +}); + +export default app; +``` + diff --git a/src/content/docs/workers/examples/security-headers.mdx b/src/content/docs/workers/examples/security-headers.mdx index c1084abc56511b5..7c29b28be2a95eb 100644 --- a/src/content/docs/workers/examples/security-headers.mdx +++ b/src/content/docs/workers/examples/security-headers.mdx @@ -322,4 +322,108 @@ async fn fetch(req: Request, _env: Env, _ctx: Context) -> Result { .with_status(res.status_code())) } ``` + + +```ts +import { Hono } from 'hono'; + +// Define the RequestWithCf interface to add Cloudflare-specific properties +interface RequestWithCf extends Request { + cf?: { + tlsVersion?: string; + // Other CF properties can be added as needed + }; +} + +const app = new Hono(); + +// Middleware to add security headers +app.use('*', async (c, next) => { + // Define security headers + const DEFAULT_SECURITY_HEADERS = { + /* + Secure your application with Content-Security-Policy headers. + Enabling these headers will permit content from a trusted domain and all its subdomains. + @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy + */ + "Content-Security-Policy": "default-src 'self' example.com *.example.com", + + /* + You can also set Strict-Transport-Security headers. + These are not automatically set because your website might get added to Chrome's HSTS preload list. + */ + "Strict-Transport-Security": "max-age=63072000; includeSubDomains; preload", + + /* + Permissions-Policy header provides the ability to allow or deny the use of browser features + */ + "Permissions-Policy": "interest-cohort=()", + + /* + X-XSS-Protection header prevents a page from loading if an XSS attack is detected. + @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection + */ + "X-XSS-Protection": "0", + + /* + X-Frame-Options header prevents click-jacking attacks. + @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options + */ + "X-Frame-Options": "DENY", + + /* + X-Content-Type-Options header prevents MIME-sniffing. + @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options + */ + "X-Content-Type-Options": "nosniff", + + "Referrer-Policy": "strict-origin-when-cross-origin", + "Cross-Origin-Embedder-Policy": 'require-corp; report-to="default";', + "Cross-Origin-Opener-Policy": 'same-site; report-to="default";', + "Cross-Origin-Resource-Policy": "same-site", + }; + + const BLOCKED_HEADERS = [ + "Public-Key-Pins", + "X-Powered-By", + "X-AspNet-Version", + ]; + + // Check TLS version from Cloudflare-specific properties + const request = c.req.raw as RequestWithCf; + const tlsVersion = request.cf?.tlsVersion; + + // Enforce TLS version 1.2 or higher + if (tlsVersion !== "TLSv1.2" && tlsVersion !== "TLSv1.3") { + return c.text("You need to use TLS version 1.2 or higher.", 400); + } + + // Process the request with the normal handler + await next(); + + // Skip if not HTML response + const contentType = c.res.headers.get("Content-Type") || ""; + if (!contentType.includes("text/html")) { + return; + } + + // Add security headers + Object.entries(DEFAULT_SECURITY_HEADERS).forEach(([name, value]) => { + c.res.headers.set(name, value); + }); + + // Remove blocked headers + BLOCKED_HEADERS.forEach(name => { + c.res.headers.delete(name); + }); +}); + +// Handle all other requests by passing through to origin +app.all('*', async (c) => { + return fetch(c.req.raw); +}); + +export default app; +``` + \ No newline at end of file diff --git a/src/content/docs/workers/examples/signing-requests.mdx b/src/content/docs/workers/examples/signing-requests.mdx index e4706525d09d569..7e589ca72f7bde5 100644 --- a/src/content/docs/workers/examples/signing-requests.mdx +++ b/src/content/docs/workers/examples/signing-requests.mdx @@ -244,6 +244,214 @@ export default { } satisfies ExportedHandler; ``` + + +```ts +import { Buffer } from "node:buffer"; +import { Hono } from 'hono'; + +const encoder = new TextEncoder(); + +// How long an HMAC token should be valid for, in seconds +const EXPIRY = 60; + +interface Env { + SECRET_DATA: string; +} + +const app = new Hono<{ Bindings: Env }>(); + +// Handle URL generation requests +app.get('/generate/*', async (c) => { + const env = c.env; + + // You will need some secret data to use as a symmetric key + const secretKeyData = encoder.encode( + env.SECRET_DATA ?? "my secret symmetric key" + ); + + // Import the secret as a CryptoKey for both 'sign' and 'verify' operations + const key = await crypto.subtle.importKey( + "raw", + secretKeyData, + { name: "HMAC", hash: "SHA-256" }, + false, + ["sign", "verify"] + ); + + const url = new URL(c.req.url); + + // Replace "/generate/" prefix with "/" + url.pathname = url.pathname.replace("/generate/", "/"); + + const timestamp = Math.floor(Date.now() / 1000); + + // Data to authenticate: pathname + timestamp + const dataToAuthenticate = `${url.pathname}${timestamp}`; + + // Sign the data + const mac = await crypto.subtle.sign( + "HMAC", + key, + encoder.encode(dataToAuthenticate) + ); + + // Convert the signature to base64 + const base64Mac = Buffer.from(mac).toString("base64"); + + // Add verification parameter to URL + url.searchParams.set("verify", `${timestamp}-${base64Mac}`); + + return c.text(`${url.pathname}${url.search}`); +}); + +// Handle verification for all other requests +app.all('*', async (c) => { + const env = c.env; + const url = new URL(c.req.url); + + // You will need some secret data to use as a symmetric key + const secretKeyData = encoder.encode( + env.SECRET_DATA ?? "my secret symmetric key" + ); + + // Import the secret as a CryptoKey for both 'sign' and 'verify' operations + const key = await crypto.subtle.importKey( + "raw", + secretKeyData, + { name: "HMAC", hash: "SHA-256" }, + false, + ["sign", "verify"] + ); + + // Make sure the request has the verification parameter + if (!url.searchParams.has("verify")) { + return c.text("Missing query parameter", 403); + } + + // Extract timestamp and signature + const [timestamp, hmac] = url.searchParams.get("verify")!.split("-"); + const assertedTimestamp = Number(timestamp); + + // Recreate the data that should have been signed + const dataToAuthenticate = `${url.pathname}${assertedTimestamp}`; + + // Convert base64 signature back to ArrayBuffer + const receivedMac = Buffer.from(hmac, "base64"); + + // Verify the signature + const verified = await crypto.subtle.verify( + "HMAC", + key, + receivedMac, + encoder.encode(dataToAuthenticate) + ); + + // If verification fails, return 403 + if (!verified) { + return c.text("Invalid MAC", 403); + } + + // Check if the signature has expired + if (Date.now() / 1000 > assertedTimestamp + EXPIRY) { + return c.text( + `URL expired at ${new Date((assertedTimestamp + EXPIRY) * 1000)}`, + 403 + ); + } + + // If verification passes, proxy the request to example.com + return fetch(new URL(url.pathname, "https://example.com"), c.req.raw); +}); + +export default app; +``` + + + +```py +from pyodide.ffi import to_js as _to_js +from js import Response, URL, TextEncoder, Buffer, fetch, Object, crypto + +def to_js(x): + return _to_js(x, dict_converter=Object.fromEntries) + +encoder = TextEncoder.new() + +# How long an HMAC token should be valid for, in seconds +EXPIRY = 60 + +async def on_fetch(request, env): + # Get the secret key + secret_key_data = encoder.encode(env.SECRET_DATA if hasattr(env, "SECRET_DATA") else "my secret symmetric key") + + # Import the secret as a CryptoKey for both 'sign' and 'verify' operations + key = await crypto.subtle.importKey( + "raw", + secret_key_data, + to_js({"name": "HMAC", "hash": "SHA-256"}), + False, + ["sign", "verify"] + ) + + url = URL.new(request.url) + + if url.pathname.startswith("/generate/"): + url.pathname = url.pathname.replace("/generate/", "/", 1) + + timestamp = int(Date.now() / 1000) + + # Data to authenticate + data_to_authenticate = f"{url.pathname}{timestamp}" + + # Sign the data + mac = await crypto.subtle.sign( + "HMAC", + key, + encoder.encode(data_to_authenticate) + ) + + # Convert to base64 + base64_mac = Buffer.from(mac).toString("base64") + + # Set the verification parameter + url.searchParams.set("verify", f"{timestamp}-{base64_mac}") + + return Response.new(f"{url.pathname}{url.search}") + else: + # Verify the request + if not "verify" in url.searchParams: + return Response.new("Missing query parameter", status=403) + + verify_param = url.searchParams.get("verify") + timestamp, hmac = verify_param.split("-") + + asserted_timestamp = int(timestamp) + + data_to_authenticate = f"{url.pathname}{asserted_timestamp}" + + received_mac = Buffer.from(hmac, "base64") + + # Verify the signature + verified = await crypto.subtle.verify( + "HMAC", + key, + received_mac, + encoder.encode(data_to_authenticate) + ) + + if not verified: + return Response.new("Invalid MAC", status=403) + + # Check expiration + if Date.now() / 1000 > asserted_timestamp + EXPIRY: + expiry_date = Date.new((asserted_timestamp + EXPIRY) * 1000) + return Response.new(f"URL expired at {expiry_date}", status=403) + + # Proxy to example.com if verification passes + return fetch(URL.new(f"https://example.com{url.pathname}"), request) +``` + ## Validate signed requests using the WAF diff --git a/src/content/docs/workers/examples/turnstile-html-rewriter.mdx b/src/content/docs/workers/examples/turnstile-html-rewriter.mdx index 1af99ecb67bb9fb..5d9f048ffdb7e41 100644 --- a/src/content/docs/workers/examples/turnstile-html-rewriter.mdx +++ b/src/content/docs/workers/examples/turnstile-html-rewriter.mdx @@ -95,6 +95,120 @@ export default { } satisfies ExportedHandler; ``` + + +```ts +import { Hono } from 'hono'; +import { html } from 'hono/html'; + +interface Env { + SITE_KEY: string; + TURNSTILE_ATTR_NAME?: string; +} + +const app = new Hono<{ Bindings: Env }>(); + +// Middleware to inject Turnstile widget +app.use('*', async (c, next) => { + const SITE_KEY = c.env.SITE_KEY; // The Turnstile Sitekey from environment + const TURNSTILE_ATTR_NAME = c.env.TURNSTILE_ATTR_NAME || "your_id_to_replace"; // The target element ID + + // Process the request through the original endpoint + await next(); + + // Only process HTML responses + const contentType = c.res.headers.get('content-type'); + if (!contentType || !contentType.includes('text/html')) { + return; + } + + // Clone the response to make it modifiable + const originalResponse = c.res; + const responseBody = await originalResponse.text(); + + // Create an HTMLRewriter instance to modify the HTML + const rewriter = new HTMLRewriter() + // Add the Turnstile script to the head + .on('head', { + element(element) { + element.append( + ``, + { html: true } + ); + } + }) + // Add the Turnstile widget to the target div + .on('div', { + element(element) { + if (element.getAttribute('id') === TURNSTILE_ATTR_NAME) { + element.append( + `
`, + { html: true } + ); + } + } + }); + + // Create a new response with the same properties as the original + const modifiedResponse = new Response(responseBody, { + status: originalResponse.status, + statusText: originalResponse.statusText, + headers: originalResponse.headers + }); + + // Transform the response using HTMLRewriter + c.res = rewriter.transform(modifiedResponse); +}); + +// Handle POST requests for form submission with Turnstile validation +app.post('*', async (c) => { + const formData = await c.req.formData(); + const token = formData.get('cf-turnstile-response'); + const ip = c.req.header('CF-Connecting-IP'); + + // If no token, return an error + if (!token) { + return c.text('Missing Turnstile token', 400); + } + + // Prepare verification data + const verifyFormData = new FormData(); + verifyFormData.append('secret', c.env.SECRET_KEY || ''); + verifyFormData.append('response', token.toString()); + if (ip) verifyFormData.append('remoteip', ip); + + // Verify the token with Turnstile API + const verifyResult = await fetch( + 'https://challenges.cloudflare.com/turnstile/v0/siteverify', + { + method: 'POST', + body: verifyFormData + } + ); + + const outcome = await verifyResult.json(); + + // If verification fails, return an error + if (!outcome.success) { + return c.text('The provided Turnstile token was not valid!', 401); + } + + // If verification succeeds, proceed with the original request + // You would typically handle the form submission logic here + + // For this example, we'll just send a success response + return c.text('Form submission successful!'); +}); + +// Default handler for GET requests +app.get('*', async (c) => { + // Fetch the original content (you'd replace this with your actual content source) + return await fetch(c.req.raw); +}); + +export default app; +``` +
```py diff --git a/src/content/docs/workers/examples/websockets.mdx b/src/content/docs/workers/examples/websockets.mdx index 65ea602e4e89be4..c16a497c0d0b66c 100644 --- a/src/content/docs/workers/examples/websockets.mdx +++ b/src/content/docs/workers/examples/websockets.mdx @@ -234,6 +234,54 @@ async fn fetch(req: HttpRequest, _env: Env, _ctx: Context) -> Result + +```ts +import { Hono } from 'hono'; + +const app = new Hono(); + +app.get('*', async (c) => { + const upgradeHeader = c.req.header('Upgrade'); + + // Check if this is a WebSocket request + if (!upgradeHeader || upgradeHeader !== 'websocket') { + return c.text('Expected Upgrade: websocket', 426); + } + + // Create a new WebSocketPair + const webSocketPair = new WebSocketPair(); + const [client, server] = Object.values(webSocketPair); + + // Accept the WebSocket connection + server.accept(); + + // Add event listeners to handle WebSocket events + server.addEventListener('message', event => { + console.log('Received message from client:', event.data); + + // Echo the message back to the client + server.send(`Echo: ${event.data}`); + }); + + server.addEventListener('close', event => { + console.log('WebSocket closed:', event); + }); + + server.addEventListener('error', event => { + console.error('WebSocket error:', event); + }); + + // Return the client WebSocket as part of the response + return new Response(null, { + status: 101, + webSocket: client, + }); +}); + +export default app; +``` + ### Connect to the WebSocket server from a client From 22a1519b12f1569ed5d94e9b7bbe56505b285de2 Mon Sep 17 00:00:00 2001 From: Lucas Kostka Date: Tue, 1 Apr 2025 10:02:07 +0200 Subject: [PATCH 2/5] Added a hono version of workers examples - fixes --- .../docs/workers/examples/ab-testing.mdx | 107 +++--- .../docs/workers/examples/basic-auth.mdx | 88 ++--- .../workers/examples/bulk-origin-proxy.mdx | 41 ++- .../docs/workers/examples/cache-api.mdx | 62 +--- .../workers/examples/cache-post-request.mdx | 91 +++--- .../docs/workers/examples/cache-tags.mdx | 52 ++- .../workers/examples/conditional-response.mdx | 125 ++++--- .../workers/examples/cors-header-proxy.mdx | 305 +++++++++--------- .../workers/examples/openai-sdk-streaming.mdx | 54 ++-- .../workers/examples/security-headers.mdx | 183 +++-------- .../workers/examples/signing-requests.mdx | 233 +++++++------ .../docs/workers/examples/websockets.mdx | 226 ++++++------- 12 files changed, 671 insertions(+), 896 deletions(-) diff --git a/src/content/docs/workers/examples/ab-testing.mdx b/src/content/docs/workers/examples/ab-testing.mdx index 3cba89d148c112c..a4c8dfe168517bc 100644 --- a/src/content/docs/workers/examples/ab-testing.mdx +++ b/src/content/docs/workers/examples/ab-testing.mdx @@ -140,65 +140,66 @@ async def on_fetch(request): ```ts -import { Hono } from 'hono'; -import { getCookie, setCookie } from 'hono/cookie'; +import { Hono } from "hono"; +import { getCookie, setCookie } from "hono/cookie"; const app = new Hono(); const NAME = "myExampleWorkersABTest"; // Middleware to handle A/B testing logic -app.use('*', async (c) => { - const url = new URL(c.req.url); - - // Enable Passthrough to allow direct access to control and test routes - if (url.pathname.startsWith("/control") || url.pathname.startsWith("/test")) { - return fetch(c.req.raw); - } - - // Determine which group this requester is in - const abTestCookie = getCookie(c, NAME); - - if (abTestCookie === 'control') { - // User is in control group - url.pathname = "/control" + url.pathname; - return fetch(url); - } else if (abTestCookie === 'test') { - // User is in test group - url.pathname = "/test" + url.pathname; - return fetch(url); - } else { - // If there is no cookie, this is a new client - // Choose a group and set the cookie (50/50 split) - const group = Math.random() < 0.5 ? "test" : "control"; - - // Update URL path based on assigned group - if (group === "control") { - url.pathname = "/control" + url.pathname; - } else { - url.pathname = "/test" + url.pathname; - } - - // Fetch from origin with modified path - const res = await fetch(url); - - // Create a new response to avoid immutability issues - const newResponse = new Response(res.body, res); - - // Set cookie to enable persistent A/B sessions - setCookie(c, NAME, group, { - path: '/', - // Add additional cookie options as needed: - // secure: true, - // httpOnly: true, - // sameSite: 'strict', - }); - - // Copy the Set-Cookie header to the response - newResponse.headers.set('Set-Cookie', c.res.headers.get('Set-Cookie') || ''); - - return newResponse; - } +app.use("*", async (c) => { + // Enable Passthrough to allow direct access to control and test routes + if (c.req.path.startsWith("/control") || c.req.path.startsWith("/test")) { + return fetch(c.req.raw); + } + + // Determine which group this requester is in + const abTestCookie = getCookie(c, NAME); + + if (abTestCookie === "control") { + // User is in control group + c.req.path = "/control" + c.req.path; + return fetch(url); + } else if (abTestCookie === "test") { + // User is in test group + url.pathname = "/test" + url.pathname; + return fetch(c.req.url); + } else { + // If there is no cookie, this is a new client + // Choose a group and set the cookie (50/50 split) + const group = Math.random() < 0.5 ? "test" : "control"; + + // Update URL path based on assigned group + if (group === "control") { + c.req.path = "/control" + c.req.path; + } else { + c.req.path = "/test" + c.req.path; + } + + // Fetch from origin with modified path + const res = await fetch(c.req.url); + + // Create a new response to avoid immutability issues + const newResponse = new Response(res.body, res); + + // Set cookie to enable persistent A/B sessions + setCookie(c, NAME, group, { + path: "/", + // Add additional cookie options as needed: + // secure: true, + // httpOnly: true, + // sameSite: 'strict', + }); + + // Copy the Set-Cookie header to the response + newResponse.headers.set( + "Set-Cookie", + c.res.headers.get("Set-Cookie") || "", + ); + + return newResponse; + } }); export default app; diff --git a/src/content/docs/workers/examples/basic-auth.mdx b/src/content/docs/workers/examples/basic-auth.mdx index 693cd5d171fe7d6..dededd9e771e182 100644 --- a/src/content/docs/workers/examples/basic-auth.mdx +++ b/src/content/docs/workers/examples/basic-auth.mdx @@ -266,15 +266,15 @@ use worker::*; #[event(fetch)] async fn fetch(req: Request, env: Env, _ctx: Context) -> Result { - let basic_user = "admin"; - // You will need an admin password. This should be - // attached to your Worker as an encrypted secret. - // Refer to https://developers.cloudflare.com/workers/configuration/secrets/ - let basic_pass = match env.secret("PASSWORD") { - Ok(s) => s.to_string(), - Err(_) => "password".to_string(), - }; - let url = req.url()?; +let basic_user = "admin"; +// You will need an admin password. This should be +// attached to your Worker as an encrypted secret. +// Refer to https://developers.cloudflare.com/workers/configuration/secrets/ +let basic_pass = match env.secret("PASSWORD") { +Ok(s) => s.to_string(), +Err(_) => "password".to_string(), +}; +let url = req.url()?; match url.path() { "/" => Response::ok("Anyone can access the homepage."), @@ -328,8 +328,10 @@ async fn fetch(req: Request, env: Env, _ctx: Context) -> Result { } _ => Response::error("Not Found.", 404), } + } -``` + +```` ```ts @@ -341,7 +343,6 @@ async fn fetch(req: Request, env: Env, _ctx: Context) -> Result { import { Hono } from 'hono'; import { auth } from 'hono/basic-auth'; -import { Buffer } from "node:buffer"; // Define environment interface interface Env { @@ -352,21 +353,6 @@ interface Env { const app = new Hono(); -// Helper function for comparing strings safely -const encoder = new TextEncoder(); -function timingSafeEqual(a: string, b: string) { - const aBytes = encoder.encode(a); - const bBytes = encoder.encode(b); - - if (aBytes.byteLength !== bBytes.byteLength) { - // Strings must be the same length in order to compare - // with crypto.subtle.timingSafeEqual - return false; - } - - return crypto.subtle.timingSafeEqual(aBytes, bBytes); -} - // Public homepage - accessible to everyone app.get('/', (c) => { return c.text("Anyone can access the homepage."); @@ -374,52 +360,14 @@ app.get('/', (c) => { // Logout route app.get('/logout', (c) => { - // Invalidate the "Authorization" header by returning a HTTP 401. - // We do not send a "WWW-Authenticate" header, as this would trigger - // a popup in the browser, immediately asking for credentials again. return c.text("Logged out.", 401); }); // Admin route - protected with Basic Auth -app.get('/admin', async (c) => { - const BASIC_USER = "admin"; - - // You will need an admin password. This should be - // attached to your Worker as an encrypted secret. - // Refer to https://developers.cloudflare.com/workers/configuration/secrets/ - const BASIC_PASS = c.env.PASSWORD ?? "password"; - - // The "Authorization" header is sent when authenticated - const authorization = c.req.header('Authorization'); - if (!authorization) { - // Prompts the user for credentials - return c.text("You need to login.", 401, { - 'WWW-Authenticate': 'Basic realm="my scope", charset="UTF-8"', - }); - } - - const [scheme, encoded] = authorization.split(" "); - - // The Authorization header must start with Basic, followed by a space - if (!encoded || scheme !== "Basic") { - return c.text("Malformed authorization header.", 400); - } - - const credentials = Buffer.from(encoded, "base64").toString(); - - // The username & password are split by the first colon - //=> example: "username:password" - const index = credentials.indexOf(":"); - const user = credentials.substring(0, index); - const pass = credentials.substring(index + 1); - - if (!timingSafeEqual(BASIC_USER, user) || !timingSafeEqual(BASIC_PASS, pass)) { - // Prompts the user for credentials again - return c.text("You need to login.", 401, { - 'WWW-Authenticate': 'Basic realm="my scope", charset="UTF-8"', - }); - } - +app.get('/admin', + basicAuth({ username: BASIC_USER, password: c.env.PASSWORD }), + async (c) => { + // Success! User is authenticated return c.text("🎉 You have private access!", 200, { 'Cache-Control': 'no-store', @@ -432,6 +380,6 @@ app.notFound((c) => { }); export default app; -``` +```` - \ No newline at end of file + diff --git a/src/content/docs/workers/examples/bulk-origin-proxy.mdx b/src/content/docs/workers/examples/bulk-origin-proxy.mdx index c5091e4a297e506..51dc87f741465b2 100644 --- a/src/content/docs/workers/examples/bulk-origin-proxy.mdx +++ b/src/content/docs/workers/examples/bulk-origin-proxy.mdx @@ -77,32 +77,31 @@ export default { ```ts -import { Hono } from 'hono'; - -type Bindings = {}; +import { Hono } from "hono"; +import { proxy } from "hono/proxy"; // An object with different URLs to fetch const ORIGINS: Record = { - "starwarsapi.yourdomain.com": "swapi.dev", - "google.yourdomain.com": "www.google.com", + "starwarsapi.yourdomain.com": "swapi.dev", + "google.yourdomain.com": "www.google.com", }; -const app = new Hono<{ Bindings: Bindings }>(); - -app.all('*', async (c) => { - const url = new URL(c.req.url); - - // Check if incoming hostname is a key in the ORIGINS object - if (url.hostname in ORIGINS) { - const target = ORIGINS[url.hostname]; - url.hostname = target; - - // If it is, proxy request to that third party origin - return fetch(url.toString(), c.req.raw); - } - - // Otherwise, process request as normal - return fetch(c.req.raw); +const app = new Hono(); + +app.all("*", async (c) => { + const url = new URL(c.req.url); + + // Check if incoming hostname is a key in the ORIGINS object + if (url.hostname in ORIGINS) { + const target = ORIGINS[url.hostname]; + url.hostname = target; + + // If it is, proxy request to that third party origin + return proxy(url, c.req.raw); + } + + // Otherwise, process request as normal + return fetch(c.req.raw); }); export default app; diff --git a/src/content/docs/workers/examples/cache-api.mdx b/src/content/docs/workers/examples/cache-api.mdx index 1b0e4b0866e3831..3bfb8e52d78a163 100644 --- a/src/content/docs/workers/examples/cache-api.mdx +++ b/src/content/docs/workers/examples/cache-api.mdx @@ -135,58 +135,28 @@ async def on_fetch(request, _env, ctx): ```ts -import { Hono } from 'hono'; -import type { Context } from 'hono'; +import { Hono } from "hono"; +import type { Context } from "hono"; +import { cache } from "hono/cache"; type Env = { - // Cloudflare Worker environment + // Cloudflare Worker environment }; -const app = new Hono<{ Bindings: Env }>(); - -app.use('*', async (c, next) => { - const request = c.req.raw; - const cacheUrl = new URL(request.url); - - // Construct the cache key from the cache URL - const cacheKey = new Request(cacheUrl.toString(), request); - const cache = caches.default; - - // Check whether the value is already available in the cache - // if not, you will need to fetch it from origin, and store it in the cache - let response = await cache.match(cacheKey); - - if (!response) { - console.log( - `Response for request url: ${request.url} not present in cache. Fetching and caching request.` - ); - - // Continue to the next middleware to handle the request - await next(); - - // Get the response that was generated - response = new Response(c.res.body, { - status: c.res.status, - headers: c.res.headers, - }); - - // Cache API respects Cache-Control headers. Setting s-max-age to 10 - // will limit the response to be in cache for 10 seconds max - response.headers.append("Cache-Control", "s-maxage=10"); - - // Use waitUntil to capture the cache.put promise - c.executionCtx.waitUntil(cache.put(cacheKey, response.clone())); - - return response; - } else { - console.log(`Cache hit for: ${request.url}.`); - return response; - } -}); +const app = new Hono(); + +// We leverage hono built-in cache helper here +app.get( + "*", + cache({ + cacheName: "my-cache", + cacheControl: "max-age=3600", // 1 hour + }), +); // Add a route to handle the request if it's not in cache -app.get('*', (c) => { - return c.text('Hello from Hono!'); +app.get("*", (c) => { + return c.text("Hello from Hono!"); }); export default app; diff --git a/src/content/docs/workers/examples/cache-post-request.mdx b/src/content/docs/workers/examples/cache-post-request.mdx index 556b1088ce62761..725722ea28d0763 100644 --- a/src/content/docs/workers/examples/cache-post-request.mdx +++ b/src/content/docs/workers/examples/cache-post-request.mdx @@ -150,65 +150,52 @@ async def on_fetch(request, _, ctx): ```ts -import { Hono } from 'hono'; +import { Hono } from "hono"; +import { sha256 } from "hono/utils/crypto"; -interface Env {} +const app = new Hono(); -const app = new Hono<{ Bindings: Env }>(); +// Middleware for caching POST requests +app.post("*", async (c) => { + try { + // Get the request body + const body = await c.req.raw.clone().text(); -// Helper function to create SHA-256 hash -async function sha256(message: string): Promise { - // Encode as UTF-8 - const msgBuffer = await new TextEncoder().encode(message); - // Hash the message - const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer); - // Convert bytes to hex string - return [...new Uint8Array(hashBuffer)] - .map((b) => b.toString(16).padStart(2, "0")) - .join(""); -} + // Hash the request body to use it as part of the cache key + const hash = await sha256(body); -// Middleware for caching POST requests -app.post('*', async (c) => { - try { - // Get the request body - const body = await c.req.raw.clone().text(); - - // Hash the request body to use it as part of the cache key - const hash = await sha256(body); - - // Create the cache URL - const cacheUrl = new URL(c.req.url); - - // Store the URL in cache by prepending the body's hash - cacheUrl.pathname = "/posts" + cacheUrl.pathname + hash; - - // Convert to a GET to be able to cache - const cacheKey = new Request(cacheUrl.toString(), { - headers: c.req.raw.headers, - method: "GET", - }); - - const cache = caches.default; - - // Find the cache key in the cache - let response = await cache.match(cacheKey); - - // If not in cache, fetch response to POST request from origin - if (!response) { - response = await fetch(c.req.raw); - c.executionCtx.waitUntil(cache.put(cacheKey, response.clone())); - } - - return response; - } catch (e) { - return c.text("Error thrown " + e.message, 500); - } + // Create the cache URL + const cacheUrl = new URL(c.req.url); + + // Store the URL in cache by prepending the body's hash + cacheUrl.pathname = "/posts" + cacheUrl.pathname + hash; + + // Convert to a GET to be able to cache + const cacheKey = new Request(cacheUrl.toString(), { + headers: c.req.raw.headers, + method: "GET", + }); + + const cache = caches.default; + + // Find the cache key in the cache + let response = await cache.match(cacheKey); + + // If not in cache, fetch response to POST request from origin + if (!response) { + response = await fetch(c.req.raw); + c.executionCtx.waitUntil(cache.put(cacheKey, response.clone())); + } + + return response; + } catch (e) { + return c.text("Error thrown " + e.message, 500); + } }); // Handle all other HTTP methods -app.all('*', (c) => { - return fetch(c.req.raw); +app.all("*", (c) => { + return fetch(c.req.raw); }); export default app; diff --git a/src/content/docs/workers/examples/cache-tags.mdx b/src/content/docs/workers/examples/cache-tags.mdx index 4b46e8b6d477e82..0b0993baf4458c3 100644 --- a/src/content/docs/workers/examples/cache-tags.mdx +++ b/src/content/docs/workers/examples/cache-tags.mdx @@ -107,40 +107,36 @@ export default { ```ts import { Hono } from "hono"; -type Bindings = {}; - -const app = new Hono<{ Bindings: Bindings }>(); +const app = new Hono(); app.all("*", async (c) => { - try { - const url = new URL(c.req.url); - const params = url.searchParams; - const tags = params.has("tags") ? params.get("tags").split(",") : []; - const uri = params.has("uri") ? params.get("uri") : ""; - - if (!uri) { - return c.json({ error: "URL cannot be empty" }, 400); - } + const tags = c.req.query("tags") ? c.req.query("tags").split(",") : []; + const uri = c.req.query("uri") ? c.req.query("uri") : ""; - const init = { - cf: { - cacheTags: tags, - }, - }; + if (!uri) { + return c.json({ error: "URL cannot be empty" }, 400); + } - const result = await fetch(uri, init); - const cacheStatus = result.headers.get("cf-cache-status"); - const lastModified = result.headers.get("last-modified"); + const init = { + cf: { + cacheTags: tags, + }, + }; - const response = { - cache: cacheStatus, - lastModified: lastModified, - }; + const result = await fetch(uri, init); + const cacheStatus = result.headers.get("cf-cache-status"); + const lastModified = result.headers.get("last-modified"); - return c.json(response, result.status); - } catch (err: any) { - return c.json({ error: err.message }, 500); - } + const response = { + cache: cacheStatus, + lastModified: lastModified, + }; + + return c.json(response, result.status); +}); + +app.onError((err, c) => { + return c.json({ error: err.message }, 500); }); export default app; diff --git a/src/content/docs/workers/examples/conditional-response.mdx b/src/content/docs/workers/examples/conditional-response.mdx index 74c76af5e3babd2..1295423b4e000c9 100644 --- a/src/content/docs/workers/examples/conditional-response.mdx +++ b/src/content/docs/workers/examples/conditional-response.mdx @@ -166,85 +166,70 @@ async def on_fetch(request): ```ts -import { Hono } from 'hono'; -import { HTTPException } from 'hono/http-exception'; - -// Define the RequestWithCf interface to add Cloudflare-specific properties -interface RequestWithCf extends Request { - cf?: { - asn?: number; - // Other CF properties can be added as needed - }; -} - -// Extend Hono's Environment interface to include our custom Request type -type Env = { - Variables: { - // Any custom variables you want to add to the context - }; -}; +import { Hono } from "hono"; +import { HTTPException } from "hono/http-exception"; -const app = new Hono(); +const app = new Hono(); // Middleware to handle all conditions before reaching the main handler -app.use('*', async (c, next) => { - const request = c.req.raw as RequestWithCf; - const BLOCKED_HOSTNAMES = ["nope.mywebsite.com", "bye.website.com"]; - - // Return a new Response based on a URL's hostname - const url = new URL(request.url); - if (BLOCKED_HOSTNAMES.includes(url.hostname)) { - return c.text("Blocked Host", 403); - } - - // Block paths ending in .doc or .xml based on the URL's file extension - const forbiddenExtRegExp = new RegExp(/\.(doc|xml)$/); - if (forbiddenExtRegExp.test(url.pathname)) { - return c.text("Blocked Extension", 403); - } - - // On User Agent - const userAgent = c.req.header("User-Agent") || ""; - if (userAgent.includes("bot")) { - return c.text("Block User Agent containing bot", 403); - } - - // On Client's IP address - const clientIP = c.req.header("CF-Connecting-IP"); - if (clientIP === "1.2.3.4") { - return c.text("Block the IP 1.2.3.4", 403); - } - - // On ASN - if (request.cf && request.cf.asn === 64512) { - return c.text("Block the ASN 64512 response"); - } - - // On Device Type - // Requires Enterprise "CF-Device-Type Header" zone setting or - // Page Rule with "Cache By Device Type" setting applied. - const device = c.req.header("CF-Device-Type"); - if (device === "mobile") { - return c.redirect("https://mobile.example.com"); - } - - // Continue to the next handler - await next(); +app.use("*", async (c, next) => { + const request = c.req.raw; + const BLOCKED_HOSTNAMES = ["nope.mywebsite.com", "bye.website.com"]; + const hostname = new URL(c.req.url)?.hostname; + + // Return a new Response based on a URL's hostname + if (BLOCKED_HOSTNAMES.includes(hostname)) { + return c.text("Blocked Host", 403); + } + + // Block paths ending in .doc or .xml based on the URL's file extension + const forbiddenExtRegExp = new RegExp(/\.(doc|xml)$/); + if (forbiddenExtRegExp.test(c.req.pathname)) { + return c.text("Blocked Extension", 403); + } + + // On User Agent + const userAgent = c.req.header("User-Agent") || ""; + if (userAgent.includes("bot")) { + return c.text("Block User Agent containing bot", 403); + } + + // On Client's IP address + const clientIP = c.req.header("CF-Connecting-IP"); + if (clientIP === "1.2.3.4") { + return c.text("Block the IP 1.2.3.4", 403); + } + + // On ASN + if (request.cf && request.cf.asn === 64512) { + return c.text("Block the ASN 64512 response"); + } + + // On Device Type + // Requires Enterprise "CF-Device-Type Header" zone setting or + // Page Rule with "Cache By Device Type" setting applied. + const device = c.req.header("CF-Device-Type"); + if (device === "mobile") { + return c.redirect("https://mobile.example.com"); + } + + // Continue to the next handler + await next(); }); // Handle POST requests differently -app.post('*', (c) => { - return c.text("Response for POST"); +app.post("*", (c) => { + return c.text("Response for POST"); }); // Default handler for other methods -app.get('*', async (c) => { - console.error( - "Getting Client's IP address, device type, and ASN are not supported in playground. Must test on a live worker" - ); - - // Fetch the original request - return fetch(c.req.raw); +app.get("*", async (c) => { + console.error( + "Getting Client's IP address, device type, and ASN are not supported in playground. Must test on a live worker", + ); + + // Fetch the original request + return fetch(c.req.raw); }); export default app; diff --git a/src/content/docs/workers/examples/cors-header-proxy.mdx b/src/content/docs/workers/examples/cors-header-proxy.mdx index 75ec9a7e2e7c95e..e25baa21e3c8f57 100644 --- a/src/content/docs/workers/examples/cors-header-proxy.mdx +++ b/src/content/docs/workers/examples/cors-header-proxy.mdx @@ -331,10 +331,8 @@ export default { ```ts -import { Hono } from 'hono'; -import { cors } from 'hono/cors'; - -type Bindings = {}; +import { Hono } from "hono"; +import { cors } from "hono/cors"; // The URL for the remote third party API you want to fetch from // but does not implement CORS @@ -343,19 +341,17 @@ const API_URL = "https://examples.cloudflareworkers.com/demos/demoapi"; // The endpoint you want the CORS reverse proxy to be on const PROXY_ENDPOINT = "/corsproxy/"; -const app = new Hono<{ Bindings: Bindings }>(); +const app = new Hono(); // Demo page handler -app.get('*', async (c) => { - const url = new URL(c.req.url); - - // Only handle non-proxy requests with this handler - if (url.pathname.startsWith(PROXY_ENDPOINT)) { - return c.next(); - } - - // Create the demo page HTML - const DEMO_PAGE = ` +app.get("*", async (c) => { + // Only handle non-proxy requests with this handler + if (c.req.path.startsWith(PROXY_ENDPOINT)) { + return next(); + } + + // Create the demo page HTML + const DEMO_PAGE = ` @@ -405,67 +401,67 @@ app.get('*', async (c) => { `; - - return c.html(DEMO_PAGE); + + return c.html(DEMO_PAGE); }); // CORS proxy routes -app.on(['GET', 'HEAD', 'POST', 'OPTIONS'], PROXY_ENDPOINT + '*', async (c) => { - const url = new URL(c.req.url); - - // Handle OPTIONS preflight requests - if (c.req.method === 'OPTIONS') { - const origin = c.req.header('Origin'); - const requestMethod = c.req.header('Access-Control-Request-Method'); - const requestHeaders = c.req.header('Access-Control-Request-Headers'); - - if (origin && requestMethod && requestHeaders) { - // Handle CORS preflight requests - return new Response(null, { - headers: { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET,HEAD,POST,OPTIONS', - 'Access-Control-Max-Age': '86400', - 'Access-Control-Allow-Headers': requestHeaders, - }, - }); - } else { - // Handle standard OPTIONS request - return new Response(null, { - headers: { - 'Allow': 'GET, HEAD, POST, OPTIONS', - }, - }); - } - } - - // Handle actual requests - let apiUrl = url.searchParams.get('apiurl') || API_URL; - - // Rewrite request to point to API URL - const modifiedRequest = new Request(apiUrl, c.req.raw); - modifiedRequest.headers.set('Origin', new URL(apiUrl).origin); - - let response = await fetch(modifiedRequest); - - // Recreate the response so we can modify the headers - response = new Response(response.body, response); - - // Set CORS headers - response.headers.set('Access-Control-Allow-Origin', url.origin); - - // Append to/Add Vary header so browser will cache response correctly - response.headers.append('Vary', 'Origin'); - - return response; +app.on(["GET", "HEAD", "POST", "OPTIONS"], PROXY_ENDPOINT + "*", async (c) => { + const url = new URL(c.req.url); + + // Handle OPTIONS preflight requests + if (c.req.method === "OPTIONS") { + const origin = c.req.header("Origin"); + const requestMethod = c.req.header("Access-Control-Request-Method"); + const requestHeaders = c.req.header("Access-Control-Request-Headers"); + + if (origin && requestMethod && requestHeaders) { + // Handle CORS preflight requests + return new Response(null, { + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET,HEAD,POST,OPTIONS", + "Access-Control-Max-Age": "86400", + "Access-Control-Allow-Headers": requestHeaders, + }, + }); + } else { + // Handle standard OPTIONS request + return new Response(null, { + headers: { + Allow: "GET, HEAD, POST, OPTIONS", + }, + }); + } + } + + // Handle actual requests + let apiUrl = url.searchParams.get("apiurl") || API_URL; + + // Rewrite request to point to API URL + const modifiedRequest = new Request(apiUrl, c.req.raw); + modifiedRequest.headers.set("Origin", new URL(apiUrl).origin); + + let response = await fetch(modifiedRequest); + + // Recreate the response so we can modify the headers + response = new Response(response.body, response); + + // Set CORS headers + response.headers.set("Access-Control-Allow-Origin", url.origin); + + // Append to/Add Vary header so browser will cache response correctly + response.headers.append("Vary", "Origin"); + + return response; }); // Handle method not allowed for proxy endpoint -app.all(PROXY_ENDPOINT + '*', (c) => { - return new Response(null, { - status: 405, - statusText: 'Method Not Allowed', - }); +app.all(PROXY_ENDPOINT + "*", (c) => { + return new Response(null, { + status: 405, + statusText: "Method Not Allowed", + }); }); export default app; @@ -585,100 +581,102 @@ async def on_fetch(request): use std::{borrow::Cow, collections::HashMap}; use worker::*; -fn raw_html_response(html: &str) -> Result { - Response::from_html(html) +fn raw*html_response(html: &str) -> Result { +Response::from_html(html) } async fn handle_request(req: Request, api_url: &str) -> Result { - let url = req.url().unwrap(); - let mut api_url2 = url - .query_pairs() - .find(|x| x.0 == Cow::Borrowed("apiurl")) - .unwrap() - .1 - .to_string(); - if api_url2 == String::from("") { - api_url2 = api_url.to_string(); - } - let mut request = req.clone_mut()?; - *request.path_mut()? = api_url2.clone(); - if let url::Origin::Tuple(origin, _, _) = Url::parse(&api_url2)?.origin() { - (*request.headers_mut()?).set("Origin", &origin)?; - } - let mut response = Fetch::Request(request).send().await?.cloned()?; - let headers = response.headers_mut(); - if let url::Origin::Tuple(origin, _, _) = url.origin() { - headers.set("Access-Control-Allow-Origin", &origin)?; - headers.set("Vary", "Origin")?; - } +let url = req.url().unwrap(); +let mut api_url2 = url +.query_pairs() +.find(|x| x.0 == Cow::Borrowed("apiurl")) +.unwrap() +.1 +.to_string(); +if api_url2 == String::from("") { +api_url2 = api_url.to_string(); +} +let mut request = req.clone_mut()?; +\*request.path_mut()? = api_url2.clone(); +if let url::Origin::Tuple(origin, *, _) = Url::parse(&api_url2)?.origin() { +(\*request.headers_mut()?).set("Origin", &origin)?; +} +let mut response = Fetch::Request(request).send().await?.cloned()?; +let headers = response.headers_mut(); +if let url::Origin::Tuple(origin, _, \_) = url.origin() { +headers.set("Access-Control-Allow-Origin", &origin)?; +headers.set("Vary", "Origin")?; +} Ok(response) + } -fn handle_options(req: Request, cors_headers: &HashMap<&str, &str>) -> Result { - let headers: Vec<_> = req.headers().keys().collect(); - if [ - "access-control-request-method", - "access-control-request-headers", - "origin", - ] - .iter() - .all(|i| headers.contains(&i.to_string())) - { - let mut headers = Headers::new(); - for (k, v) in cors_headers.iter() { - headers.set(k, v)?; - } - return Ok(Response::empty()?.with_headers(headers)); - } - Response::empty() +fn handle*options(req: Request, cors_headers: &HashMap<&str, &str>) -> Result { +let headers: Vec<*> = req.headers().keys().collect(); +if [ +"access-control-request-method", +"access-control-request-headers", +"origin", +] +.iter() +.all(|i| headers.contains(&i.to_string())) +{ +let mut headers = Headers::new(); +for (k, v) in cors_headers.iter() { +headers.set(k, v)?; } -#[event(fetch)] -async fn fetch(req: Request, _env: Env, _ctx: Context) -> Result { - let cors_headers = HashMap::from([ - ("Access-Control-Allow-Origin", "*"), - ("Access-Control-Allow-Methods", "GET,HEAD,POST,OPTIONS"), - ("Access-Control-Max-Age", "86400"), - ]); - let api_url = "https://examples.cloudflareworkers.com/demos/demoapi"; - let proxy_endpoint = "/corsproxy/"; - let demo_page = format!( - r#" - - - -

API GET without CORS Proxy

- Shows TypeError: Failed to fetch since CORS is misconfigured -

- Waiting -

API GET with CORS Proxy

-

- Waiting -

API POST with CORS Proxy + Preflight

-

- Waiting - - - - "# - ); +}} +}})() + + + +"# +); if req.url()?.path().starts_with(proxy_endpoint) { match req.method() { @@ -702,6 +700,9 @@ async fn fetch(req: Request, _env: Env, _ctx: Context) -> Result { } } raw_html_response(&demo_page) + } + +``` + ``` - \ No newline at end of file diff --git a/src/content/docs/workers/examples/openai-sdk-streaming.mdx b/src/content/docs/workers/examples/openai-sdk-streaming.mdx index 2a3df112afbfe2f..76ae5af6accdfc3 100644 --- a/src/content/docs/workers/examples/openai-sdk-streaming.mdx +++ b/src/content/docs/workers/examples/openai-sdk-streaming.mdx @@ -67,45 +67,33 @@ export default { ```ts -import { Hono } from 'hono'; -import OpenAI from 'openai'; +import { Hono } from "hono"; +import { streamText } from "hono/streaming"; +import OpenAI from "openai"; interface Env { - OPENAI_API_KEY: string; + OPENAI_API_KEY: string; } const app = new Hono<{ Bindings: Env }>(); -app.get('*', async (c) => { - const openai = new OpenAI({ - apiKey: c.env.OPENAI_API_KEY, - }); - - // Create a TransformStream to handle streaming data - let { readable, writable } = new TransformStream(); - let writer = writable.getWriter(); - const textEncoder = new TextEncoder(); - - c.executionCtx.waitUntil( - (async () => { - const stream = await openai.chat.completions.create({ - model: "gpt-4o-mini", - messages: [{ role: "user", content: "Tell me a story" }], - stream: true, - }); - - // Loop over the data as it is streamed and write to the writeable - for await (const part of stream) { - writer.write( - textEncoder.encode(part.choices[0]?.delta?.content || ""), - ); - } - writer.close(); - })() - ); - - // Send the readable back to the browser - return new Response(readable); +app.get("*", async (c) => { + const openai = new OpenAI({ + apiKey: c.env.OPENAI_API_KEY, + }); + + const chatStream = await openai.chat.completions.create({ + model: "gpt-4o-mini", + messages: [{ role: "user", content: "Tell me a story" }], + stream: true, + }); + + return streamText(c, async (stream) => { + for await (const message of chatStream) { + await stream.write(message.choices[0].delta.content || ""); + } + stream.close(); + }); }); export default app; diff --git a/src/content/docs/workers/examples/security-headers.mdx b/src/content/docs/workers/examples/security-headers.mdx index 7c29b28be2a95eb..09b409b33c30237 100644 --- a/src/content/docs/workers/examples/security-headers.mdx +++ b/src/content/docs/workers/examples/security-headers.mdx @@ -257,48 +257,48 @@ use std::collections::HashMap; use worker::*; #[event(fetch)] -async fn fetch(req: Request, _env: Env, _ctx: Context) -> Result { - let default_security_headers = HashMap::from([ - //Secure your application with Content-Security-Policy headers. - //Enabling these headers will permit content from a trusted domain and all its subdomains. - //@see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy - ( - "Content-Security-Policy", - "default-src 'self' example.com *.example.com", - ), - //You can also set Strict-Transport-Security headers. - //These are not automatically set because your website might get added to Chrome's HSTS preload list. - //Here's the code if you want to apply it: - ( - "Strict-Transport-Security", - "max-age=63072000; includeSubDomains; preload", - ), - //Permissions-Policy header provides the ability to allow or deny the use of browser features, such as opting out of FLoC - which you can use below: - ("Permissions-Policy", "interest-cohort=()"), - //X-XSS-Protection header prevents a page from loading if an XSS attack is detected. - //@see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection - ("X-XSS-Protection", "0"), - //X-Frame-Options header prevents click-jacking attacks. - //@see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options - ("X-Frame-Options", "DENY"), - //X-Content-Type-Options header prevents MIME-sniffing. - //@see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options - ("X-Content-Type-Options", "nosniff"), - ("Referrer-Policy", "strict-origin-when-cross-origin"), - ( - "Cross-Origin-Embedder-Policy", - "require-corp; report-to='default';", - ), - ( - "Cross-Origin-Opener-Policy", - "same-site; report-to='default';", - ), - ("Cross-Origin-Resource-Policy", "same-site"), - ]); - let blocked_headers = ["Public-Key-Pins", "X-Powered-By", "X-AspNet-Version"]; - let tls = req.cf().unwrap().tls_version(); - let res = Fetch::Request(req).send().await?; - let mut new_headers = res.headers().clone(); +async fn fetch(req: Request, \_env: Env, \_ctx: Context) -> Result { +let default_security_headers = HashMap::from([ +//Secure your application with Content-Security-Policy headers. +//Enabling these headers will permit content from a trusted domain and all its subdomains. +//@see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy +( +"Content-Security-Policy", +"default-src 'self' example.com *.example.com", +), +//You can also set Strict-Transport-Security headers. +//These are not automatically set because your website might get added to Chrome's HSTS preload list. +//Here's the code if you want to apply it: +( +"Strict-Transport-Security", +"max-age=63072000; includeSubDomains; preload", +), +//Permissions-Policy header provides the ability to allow or deny the use of browser features, such as opting out of FLoC - which you can use below: +("Permissions-Policy", "interest-cohort=()"), +//X-XSS-Protection header prevents a page from loading if an XSS attack is detected. +//@see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection +("X-XSS-Protection", "0"), +//X-Frame-Options header prevents click-jacking attacks. +//@see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options +("X-Frame-Options", "DENY"), +//X-Content-Type-Options header prevents MIME-sniffing. +//@see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options +("X-Content-Type-Options", "nosniff"), +("Referrer-Policy", "strict-origin-when-cross-origin"), +( +"Cross-Origin-Embedder-Policy", +"require-corp; report-to='default';", +), +( +"Cross-Origin-Opener-Policy", +"same-site; report-to='default';", +), +("Cross-Origin-Resource-Policy", "same-site"), +]); +let blocked_headers = ["Public-Key-Pins", "X-Powered-By", "X-AspNet-Version"]; +let tls = req.cf().unwrap().tls_version(); +let res = Fetch::Request(req).send().await?; +let mut new_headers = res.headers().clone(); // This sets the headers for HTML responses if Some(String::from("text/html")) == new_headers.get("Content-Type")? { @@ -320,103 +320,18 @@ async fn fetch(req: Request, _env: Env, _ctx: Context) -> Result { Ok(Response::from_body(res.body().clone())? .with_headers(new_headers) .with_status(res.status_code())) + } -``` + +```` ```ts import { Hono } from 'hono'; - -// Define the RequestWithCf interface to add Cloudflare-specific properties -interface RequestWithCf extends Request { - cf?: { - tlsVersion?: string; - // Other CF properties can be added as needed - }; -} +import { secureHeaders } from 'hono/secure-headers'; const app = new Hono(); - -// Middleware to add security headers -app.use('*', async (c, next) => { - // Define security headers - const DEFAULT_SECURITY_HEADERS = { - /* - Secure your application with Content-Security-Policy headers. - Enabling these headers will permit content from a trusted domain and all its subdomains. - @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy - */ - "Content-Security-Policy": "default-src 'self' example.com *.example.com", - - /* - You can also set Strict-Transport-Security headers. - These are not automatically set because your website might get added to Chrome's HSTS preload list. - */ - "Strict-Transport-Security": "max-age=63072000; includeSubDomains; preload", - - /* - Permissions-Policy header provides the ability to allow or deny the use of browser features - */ - "Permissions-Policy": "interest-cohort=()", - - /* - X-XSS-Protection header prevents a page from loading if an XSS attack is detected. - @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection - */ - "X-XSS-Protection": "0", - - /* - X-Frame-Options header prevents click-jacking attacks. - @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options - */ - "X-Frame-Options": "DENY", - - /* - X-Content-Type-Options header prevents MIME-sniffing. - @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options - */ - "X-Content-Type-Options": "nosniff", - - "Referrer-Policy": "strict-origin-when-cross-origin", - "Cross-Origin-Embedder-Policy": 'require-corp; report-to="default";', - "Cross-Origin-Opener-Policy": 'same-site; report-to="default";', - "Cross-Origin-Resource-Policy": "same-site", - }; - - const BLOCKED_HEADERS = [ - "Public-Key-Pins", - "X-Powered-By", - "X-AspNet-Version", - ]; - - // Check TLS version from Cloudflare-specific properties - const request = c.req.raw as RequestWithCf; - const tlsVersion = request.cf?.tlsVersion; - - // Enforce TLS version 1.2 or higher - if (tlsVersion !== "TLSv1.2" && tlsVersion !== "TLSv1.3") { - return c.text("You need to use TLS version 1.2 or higher.", 400); - } - - // Process the request with the normal handler - await next(); - - // Skip if not HTML response - const contentType = c.res.headers.get("Content-Type") || ""; - if (!contentType.includes("text/html")) { - return; - } - - // Add security headers - Object.entries(DEFAULT_SECURITY_HEADERS).forEach(([name, value]) => { - c.res.headers.set(name, value); - }); - - // Remove blocked headers - BLOCKED_HEADERS.forEach(name => { - c.res.headers.delete(name); - }); -}); +app.use(secureHeaders()); // Handle all other requests by passing through to origin app.all('*', async (c) => { @@ -424,6 +339,6 @@ app.all('*', async (c) => { }); export default app; -``` +```` - \ No newline at end of file + diff --git a/src/content/docs/workers/examples/signing-requests.mdx b/src/content/docs/workers/examples/signing-requests.mdx index 7e589ca72f7bde5..d511a27c31cd12f 100644 --- a/src/content/docs/workers/examples/signing-requests.mdx +++ b/src/content/docs/workers/examples/signing-requests.mdx @@ -248,7 +248,8 @@ export default { ```ts import { Buffer } from "node:buffer"; -import { Hono } from 'hono'; +import { Hono } from "hono"; +import { proxy } from "hono/proxy"; const encoder = new TextEncoder(); @@ -256,112 +257,110 @@ const encoder = new TextEncoder(); const EXPIRY = 60; interface Env { - SECRET_DATA: string; + SECRET_DATA: string; } -const app = new Hono<{ Bindings: Env }>(); +const app = new Hono(); // Handle URL generation requests -app.get('/generate/*', async (c) => { - const env = c.env; - - // You will need some secret data to use as a symmetric key - const secretKeyData = encoder.encode( - env.SECRET_DATA ?? "my secret symmetric key" - ); - - // Import the secret as a CryptoKey for both 'sign' and 'verify' operations - const key = await crypto.subtle.importKey( - "raw", - secretKeyData, - { name: "HMAC", hash: "SHA-256" }, - false, - ["sign", "verify"] - ); - - const url = new URL(c.req.url); - - // Replace "/generate/" prefix with "/" - url.pathname = url.pathname.replace("/generate/", "/"); - - const timestamp = Math.floor(Date.now() / 1000); - - // Data to authenticate: pathname + timestamp - const dataToAuthenticate = `${url.pathname}${timestamp}`; - - // Sign the data - const mac = await crypto.subtle.sign( - "HMAC", - key, - encoder.encode(dataToAuthenticate) - ); - - // Convert the signature to base64 - const base64Mac = Buffer.from(mac).toString("base64"); - - // Add verification parameter to URL - url.searchParams.set("verify", `${timestamp}-${base64Mac}`); - - return c.text(`${url.pathname}${url.search}`); +app.get("/generate/*", async (c) => { + const env = c.env; + + // You will need some secret data to use as a symmetric key + const secretKeyData = encoder.encode( + env.SECRET_DATA ?? "my secret symmetric key", + ); + + // Import the secret as a CryptoKey for both 'sign' and 'verify' operations + const key = await crypto.subtle.importKey( + "raw", + secretKeyData, + { name: "HMAC", hash: "SHA-256" }, + false, + ["sign", "verify"], + ); + + // Replace "/generate/" prefix with "/" + let pathname = c.req.path.replace("/generate/", "/"); + + const timestamp = Math.floor(Date.now() / 1000); + + // Data to authenticate: pathname + timestamp + const dataToAuthenticate = `${pathname}${timestamp}`; + + // Sign the data + const mac = await crypto.subtle.sign( + "HMAC", + key, + encoder.encode(dataToAuthenticate), + ); + + // Convert the signature to base64 + const base64Mac = Buffer.from(mac).toString("base64"); + + // Add verification parameter to URL + url.searchParams.set("verify", `${timestamp}-${base64Mac}`); + + return c.text(`${pathname}${url.search}`); }); // Handle verification for all other requests -app.all('*', async (c) => { - const env = c.env; - const url = new URL(c.req.url); - - // You will need some secret data to use as a symmetric key - const secretKeyData = encoder.encode( - env.SECRET_DATA ?? "my secret symmetric key" - ); - - // Import the secret as a CryptoKey for both 'sign' and 'verify' operations - const key = await crypto.subtle.importKey( - "raw", - secretKeyData, - { name: "HMAC", hash: "SHA-256" }, - false, - ["sign", "verify"] - ); - - // Make sure the request has the verification parameter - if (!url.searchParams.has("verify")) { - return c.text("Missing query parameter", 403); - } - - // Extract timestamp and signature - const [timestamp, hmac] = url.searchParams.get("verify")!.split("-"); - const assertedTimestamp = Number(timestamp); - - // Recreate the data that should have been signed - const dataToAuthenticate = `${url.pathname}${assertedTimestamp}`; - - // Convert base64 signature back to ArrayBuffer - const receivedMac = Buffer.from(hmac, "base64"); - - // Verify the signature - const verified = await crypto.subtle.verify( - "HMAC", - key, - receivedMac, - encoder.encode(dataToAuthenticate) - ); - - // If verification fails, return 403 - if (!verified) { - return c.text("Invalid MAC", 403); - } - - // Check if the signature has expired - if (Date.now() / 1000 > assertedTimestamp + EXPIRY) { - return c.text( - `URL expired at ${new Date((assertedTimestamp + EXPIRY) * 1000)}`, - 403 - ); - } - - // If verification passes, proxy the request to example.com - return fetch(new URL(url.pathname, "https://example.com"), c.req.raw); +app.all("*", async (c) => { + const env = c.env; + const url = c.req.url) + + // You will need some secret data to use as a symmetric key + const secretKeyData = encoder.encode( + env.SECRET_DATA ?? "my secret symmetric key", + ); + + // Import the secret as a CryptoKey for both 'sign' and 'verify' operations + const key = await crypto.subtle.importKey( + "raw", + secretKeyData, + { name: "HMAC", hash: "SHA-256" }, + false, + ["sign", "verify"], + ); + + // Make sure the request has the verification parameter + if (!c.req.query("verify")) { + return c.text("Missing query parameter", 403); + } + + // Extract timestamp and signature + const [timestamp, hmac] = c.req.query("verify")!.split("-"); + const assertedTimestamp = Number(timestamp); + + // Recreate the data that should have been signed + const dataToAuthenticate = `${c.req.path}${assertedTimestamp}`; + + // Convert base64 signature back to ArrayBuffer + const receivedMac = Buffer.from(hmac, "base64"); + + // Verify the signature + const verified = await crypto.subtle.verify( + "HMAC", + key, + receivedMac, + encoder.encode(dataToAuthenticate), + ); + + // If verification fails, return 403 + if (!verified) { + return c.text("Invalid MAC", 403); + } + + // Check if the signature has expired + if (Date.now() / 1000 > assertedTimestamp + EXPIRY) { + return c.text( + `URL expired at ${new Date((assertedTimestamp + EXPIRY) * 1000)}`, + 403, + ); + } + + // If verification passes, proxy the request to example.com + return proxy(`https://example.com/${c.req.path}`, ...c.req); }); export default app; @@ -384,7 +383,7 @@ EXPIRY = 60 async def on_fetch(request, env): # Get the secret key secret_key_data = encoder.encode(env.SECRET_DATA if hasattr(env, "SECRET_DATA") else "my secret symmetric key") - + # Import the secret as a CryptoKey for both 'sign' and 'verify' operations key = await crypto.subtle.importKey( "raw", @@ -393,45 +392,45 @@ async def on_fetch(request, env): False, ["sign", "verify"] ) - + url = URL.new(request.url) - + if url.pathname.startswith("/generate/"): url.pathname = url.pathname.replace("/generate/", "/", 1) - + timestamp = int(Date.now() / 1000) - + # Data to authenticate data_to_authenticate = f"{url.pathname}{timestamp}" - + # Sign the data mac = await crypto.subtle.sign( "HMAC", key, encoder.encode(data_to_authenticate) ) - + # Convert to base64 base64_mac = Buffer.from(mac).toString("base64") - + # Set the verification parameter url.searchParams.set("verify", f"{timestamp}-{base64_mac}") - + return Response.new(f"{url.pathname}{url.search}") else: # Verify the request if not "verify" in url.searchParams: return Response.new("Missing query parameter", status=403) - + verify_param = url.searchParams.get("verify") timestamp, hmac = verify_param.split("-") - + asserted_timestamp = int(timestamp) - + data_to_authenticate = f"{url.pathname}{asserted_timestamp}" - + received_mac = Buffer.from(hmac, "base64") - + # Verify the signature verified = await crypto.subtle.verify( "HMAC", @@ -439,15 +438,15 @@ async def on_fetch(request, env): received_mac, encoder.encode(data_to_authenticate) ) - + if not verified: return Response.new("Invalid MAC", status=403) - + # Check expiration if Date.now() / 1000 > asserted_timestamp + EXPIRY: expiry_date = Date.new((asserted_timestamp + EXPIRY) * 1000) return Response.new(f"URL expired at {expiry_date}", status=403) - + # Proxy to example.com if verification passes return fetch(URL.new(f"https://example.com{url.pathname}"), request) ``` diff --git a/src/content/docs/workers/examples/websockets.mdx b/src/content/docs/workers/examples/websockets.mdx index c16a497c0d0b66c..b519f2826f18958 100644 --- a/src/content/docs/workers/examples/websockets.mdx +++ b/src/content/docs/workers/examples/websockets.mdx @@ -21,18 +21,14 @@ WebSockets are open connections sustained between the client and the origin serv :::note - WebSockets utilize an event-based system for receiving and sending messages, much like the Workers runtime model of responding to events. - ::: :::note - If your application needs to coordinate among multiple WebSocket connections, such as a chat room or game match, you will need clients to send messages to a single-point-of-coordination. Durable Objects provide a single-point-of-coordination for Cloudflare Workers, and are often used in parallel with WebSockets to persist state over multiple clients and connections. In this case, refer to [Durable Objects](/durable-objects/) to get started, and prefer using the Durable Objects' extended [WebSockets API](/durable-objects/best-practices/websockets/). - ::: ## Write a WebSocket Server @@ -43,15 +39,15 @@ A client can make a WebSocket request in the browser by instantiating a new inst ```js // In client-side JavaScript, connect to your Workers function using WebSockets: -const websocket = new WebSocket('wss://example-websocket.signalnerve.workers.dev'); +const websocket = new WebSocket( + "wss://example-websocket.signalnerve.workers.dev", +); ``` :::note - For more details about creating and working with WebSockets in the client, refer to [Writing a WebSocket client](#write-a-websocket-client). - ::: When an incoming WebSocket request reaches the Workers function, it will contain an `Upgrade` header, set to the string value `websocket`. Check for this header before continuing to instantiate a WebSocket: @@ -68,19 +64,20 @@ async function handleRequest(request) { ```rs -use worker::*; +use worker::\*; #[event(fetch)] -async fn fetch(req: HttpRequest, _env: Env, _ctx: Context) -> Result { - let upgrade_header = match req.headers().get("Upgrade") { - Some(h) => h.to_str().unwrap(), - None => "", - }; - if upgrade_header != "websocket" { - return worker::Response::error("Expected Upgrade: websocket", 426); - } +async fn fetch(req: HttpRequest, \_env: Env, \_ctx: Context) -> Result { +let upgrade_header = match req.headers().get("Upgrade") { +Some(h) => h.to_str().unwrap(), +None => "", +}; +if upgrade_header != "websocket" { +return worker::Response::error("Expected Upgrade: websocket", 426); } -``` +} + +```` After you have appropriately checked for the `Upgrade` header, you can create a new instance of `WebSocketPair`, which contains server and client WebSockets. One of these WebSockets should be handled by the Workers function and the other should be returned as part of a `Response` with the [`101` status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/101), indicating the request is switching protocols: @@ -102,20 +99,21 @@ async function handleRequest(request) { webSocket: client, }); } -``` +```` + ```rs use worker::*; #[event(fetch)] -async fn fetch(req: HttpRequest, _env: Env, _ctx: Context) -> Result { - let upgrade_header = match req.headers().get("Upgrade") { - Some(h) => h.to_str().unwrap(), - None => "", - }; - if upgrade_header != "websocket" { - return worker::Response::error("Expected Upgrade: websocket", 426); - } +async fn fetch(req: HttpRequest, \_env: Env, \_ctx: Context) -> Result { +let upgrade_header = match req.headers().get("Upgrade") { +Some(h) => h.to_str().unwrap(), +None => "", +}; +if upgrade_header != "websocket" { +return worker::Response::error("Expected Upgrade: websocket", 426); +} let ws = WebSocketPair::new()?; let client = ws.client; @@ -123,9 +121,10 @@ async fn fetch(req: HttpRequest, _env: Env, _ctx: Context) -> Result The `WebSocketPair` constructor returns an Object, with the `0` and `1` keys each holding a `WebSocket` instance as its value. It is common to grab the two WebSockets from this pair using [`Object.values`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_objects/Object/values) and [ES6 destructuring](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment), as seen in the below example. @@ -151,20 +150,21 @@ async function handleRequest(request) { webSocket: client, }); } -``` +```` + ```rs use worker::*; #[event(fetch)] -async fn fetch(req: HttpRequest, _env: Env, _ctx: Context) -> Result { - let upgrade_header = match req.headers().get("Upgrade") { - Some(h) => h.to_str().unwrap(), - None => "", - }; - if upgrade_header != "websocket" { - return worker::Response::error("Expected Upgrade: websocket", 426); - } +async fn fetch(req: HttpRequest, \_env: Env, \_ctx: Context) -> Result { +let upgrade_header = match req.headers().get("Upgrade") { +Some(h) => h.to_str().unwrap(), +None => "", +}; +if upgrade_header != "websocket" { +return worker::Response::error("Expected Upgrade: websocket", 426); +} let ws = WebSocketPair::new()?; let client = ws.client; @@ -172,9 +172,10 @@ async fn fetch(req: HttpRequest, _env: Env, _ctx: Context) -> Result WebSockets emit a number of [Events](/workers/runtime-apis/websockets/#events) that can be connected to using `addEventListener`. The below example hooks into the `message` event and emits a `console.log` with the data from it: @@ -200,21 +201,22 @@ async function handleRequest(request) { webSocket: client, }); } -``` +```` + ```rs use futures::StreamExt; use worker::*; #[event(fetch)] -async fn fetch(req: HttpRequest, _env: Env, _ctx: Context) -> Result { - let upgrade_header = match req.headers().get("Upgrade") { - Some(h) => h.to_str().unwrap(), - None => "", - }; - if upgrade_header != "websocket" { - return worker::Response::error("Expected Upgrade: websocket", 426); - } +async fn fetch(req: HttpRequest, \_env: Env, \_ctx: Context) -> Result { +let upgrade_header = match req.headers().get("Upgrade") { +Some(h) => h.to_str().unwrap(), +None => "", +}; +if upgrade_header != "websocket" { +return worker::Response::error("Expected Upgrade: websocket", 426); +} let ws = WebSocketPair::new()?; let client = ws.client; @@ -231,56 +233,38 @@ async fn fetch(req: HttpRequest, _env: Env, _ctx: Context) -> Result ```ts -import { Hono } from 'hono'; - -const app = new Hono(); - -app.get('*', async (c) => { - const upgradeHeader = c.req.header('Upgrade'); - - // Check if this is a WebSocket request - if (!upgradeHeader || upgradeHeader !== 'websocket') { - return c.text('Expected Upgrade: websocket', 426); - } - - // Create a new WebSocketPair - const webSocketPair = new WebSocketPair(); - const [client, server] = Object.values(webSocketPair); - - // Accept the WebSocket connection - server.accept(); - - // Add event listeners to handle WebSocket events - server.addEventListener('message', event => { - console.log('Received message from client:', event.data); - - // Echo the message back to the client - server.send(`Echo: ${event.data}`); - }); - - server.addEventListener('close', event => { - console.log('WebSocket closed:', event); - }); - - server.addEventListener('error', event => { - console.error('WebSocket error:', event); - }); - - // Return the client WebSocket as part of the response - return new Response(null, { - status: 101, - webSocket: client, - }); -}); +import { Hono } from 'hono' +import { upgradeWebSocket } from 'hono/cloudflare-workers' + +const app = new Hono() + +app.get( + '*', + upgradeWebSocket((c) => { + return { + onMessage(event, ws) { + console.log('Received message from client:', event.data) + ws.send(`Echo: ${event.data}`) + }, + onClose: () => { + console.log('WebSocket closed:', event) + }, + onError: () => { + console.error('WebSocket error:', event) + }, + } + }) +) export default app; -``` +```` @@ -289,17 +273,19 @@ export default app; Writing WebSocket clients that communicate with your Workers function is a two-step process: first, create the WebSocket instance, and then attach event listeners to it: ```js -const websocket = new WebSocket('wss://websocket-example.signalnerve.workers.dev'); -websocket.addEventListener('message', event => { - console.log('Message received from server'); - console.log(event.data); +const websocket = new WebSocket( + "wss://websocket-example.signalnerve.workers.dev", +); +websocket.addEventListener("message", (event) => { + console.log("Message received from server"); + console.log(event.data); }); ``` WebSocket clients can send messages back to the server using the [`send`](/workers/runtime-apis/websockets/#send) function: ```js -websocket.send('MESSAGE'); +websocket.send("MESSAGE"); ``` When the WebSocket interaction is complete, the client can close the connection using [`close`](/workers/runtime-apis/websockets/#close): @@ -318,34 +304,34 @@ Additionally, Cloudflare supports establishing WebSocket connections by making a ```js async function websocket(url) { - // Make a fetch request including `Upgrade: websocket` header. - // The Workers Runtime will automatically handle other requirements - // of the WebSocket protocol, like the Sec-WebSocket-Key header. - let resp = await fetch(url, { - headers: { - Upgrade: 'websocket', - }, - }); - - // If the WebSocket handshake completed successfully, then the - // response has a `webSocket` property. - let ws = resp.webSocket; - if (!ws) { - throw new Error("server didn't accept WebSocket"); - } - - // Call accept() to indicate that you'll be handling the socket here - // in JavaScript, as opposed to returning it on to a client. - ws.accept(); - - // Now you can send and receive messages like before. - ws.send('hello'); - ws.addEventListener('message', msg => { - console.log(msg.data); - }); + // Make a fetch request including `Upgrade: websocket` header. + // The Workers Runtime will automatically handle other requirements + // of the WebSocket protocol, like the Sec-WebSocket-Key header. + let resp = await fetch(url, { + headers: { + Upgrade: "websocket", + }, + }); + + // If the WebSocket handshake completed successfully, then the + // response has a `webSocket` property. + let ws = resp.webSocket; + if (!ws) { + throw new Error("server didn't accept WebSocket"); + } + + // Call accept() to indicate that you'll be handling the socket here + // in JavaScript, as opposed to returning it on to a client. + ws.accept(); + + // Now you can send and receive messages like before. + ws.send("hello"); + ws.addEventListener("message", (msg) => { + console.log(msg.data); + }); } ``` ## WebSocket compression -Cloudflare Workers supports WebSocket compression. Refer to [WebSocket Compression](/workers/configuration/compatibility-flags/#websocket-compression) for more information. \ No newline at end of file +Cloudflare Workers supports WebSocket compression. Refer to [WebSocket Compression](/workers/configuration/compatibility-flags/#websocket-compression) for more information. From f202f1f03cbec81b33b0dfe45d7d695ef192e166 Mon Sep 17 00:00:00 2001 From: Lucas Kostka Date: Thu, 3 Apr 2025 10:12:32 +0200 Subject: [PATCH 3/5] Added a hono version of woekrs examples - basic auth fix --- src/content/docs/workers/examples/basic-auth.mdx | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/content/docs/workers/examples/basic-auth.mdx b/src/content/docs/workers/examples/basic-auth.mdx index dededd9e771e182..791a50082dfa2a1 100644 --- a/src/content/docs/workers/examples/basic-auth.mdx +++ b/src/content/docs/workers/examples/basic-auth.mdx @@ -364,14 +364,8 @@ app.get('/logout', (c) => { }); // Admin route - protected with Basic Auth -app.get('/admin', - basicAuth({ username: BASIC_USER, password: c.env.PASSWORD }), - async (c) => { - - // Success! User is authenticated - return c.text("🎉 You have private access!", 200, { - 'Cache-Control': 'no-store', - }); +app.get('/admin', async (c, next) => { + return basicAuth({ username: c.env.USERNAME, password: c.env.PASSWORD })(c, next); }); // Handle 404 for any other routes From 5ee2391c7936d6b491ccd13ee0045d6fa47053b6 Mon Sep 17 00:00:00 2001 From: Lucas Kostka Date: Mon, 14 Apr 2025 08:54:40 +0200 Subject: [PATCH 4/5] PR #21258 - hono examples fixes --- .../docs/workers/examples/103-early-hints.mdx | 27 ++- .../docs/workers/examples/ab-testing.mdx | 39 ++-- .../accessing-the-cloudflare-object.mdx | 4 +- .../workers/examples/aggregate-requests.mdx | 38 ++-- .../docs/workers/examples/basic-auth.mdx | 28 +-- .../docs/workers/examples/block-on-tls.mdx | 31 ++- .../workers/examples/bulk-origin-proxy.mdx | 2 +- .../docs/workers/examples/bulk-redirects.mdx | 34 ++-- .../docs/workers/examples/cache-api.mdx | 5 - .../examples/geolocation-hello-world.mdx | 2 +- .../docs/workers/examples/logging-headers.mdx | 27 +-- .../examples/modify-request-property.mdx | 92 ++++----- .../workers/examples/signing-requests.mdx | 2 +- .../examples/turnstile-html-rewriter.mdx | 186 +++++++++--------- 14 files changed, 244 insertions(+), 273 deletions(-) diff --git a/src/content/docs/workers/examples/103-early-hints.mdx b/src/content/docs/workers/examples/103-early-hints.mdx index 58cf58b4a8d3551..4753b6f032471e7 100644 --- a/src/content/docs/workers/examples/103-early-hints.mdx +++ b/src/content/docs/workers/examples/103-early-hints.mdx @@ -139,7 +139,7 @@ def on_fetch(request): ```ts -import { Hono } from 'hono'; +import { Hono } from "hono"; const app = new Hono(); @@ -159,22 +159,21 @@ const HTML = ` `; // Serve CSS file -app.get('/test.css', (c) => { - return c.body(CSS, { - headers: { - "content-type": "text/css", - }, - }); +app.get("/test.css", (c) => { + return c.body(CSS, { + headers: { + "content-type": "text/css", + }, + }); }); // Serve HTML with early hints -app.get('*', (c) => { - return c.body(HTML, { - headers: { - "content-type": "text/html", - "link": "; rel=preload; as=style", - }, - }); +app.get("*", (c) => { + return c.html(HTML, { + headers: { + link: "; rel=preload; as=style", + }, + }); }); export default app; diff --git a/src/content/docs/workers/examples/ab-testing.mdx b/src/content/docs/workers/examples/ab-testing.mdx index a4c8dfe168517bc..1a4807ba6b7f7dc 100644 --- a/src/content/docs/workers/examples/ab-testing.mdx +++ b/src/content/docs/workers/examples/ab-testing.mdx @@ -147,24 +147,23 @@ const app = new Hono(); const NAME = "myExampleWorkersABTest"; +// Enable passthrough to allow direct access to control and test routes +app.all("/control/*", (c) => fetch(c.req.raw)); +app.all("/test/*", (c) => fetch(c.req.raw)); + // Middleware to handle A/B testing logic app.use("*", async (c) => { - // Enable Passthrough to allow direct access to control and test routes - if (c.req.path.startsWith("/control") || c.req.path.startsWith("/test")) { - return fetch(c.req.raw); - } + const url = new URL(c.req.url); // Determine which group this requester is in const abTestCookie = getCookie(c, NAME); if (abTestCookie === "control") { // User is in control group - c.req.path = "/control" + c.req.path; - return fetch(url); + url.pathname = "/control" + c.req.path; } else if (abTestCookie === "test") { // User is in test group - url.pathname = "/test" + url.pathname; - return fetch(c.req.url); + url.pathname = "/test" + c.req.path; } else { // If there is no cookie, this is a new client // Choose a group and set the cookie (50/50 split) @@ -172,34 +171,20 @@ app.use("*", async (c) => { // Update URL path based on assigned group if (group === "control") { - c.req.path = "/control" + c.req.path; + url.pathname = "/control" + c.req.path; } else { - c.req.path = "/test" + c.req.path; + url.pathname = "/test" + c.req.path; } - // Fetch from origin with modified path - const res = await fetch(c.req.url); - - // Create a new response to avoid immutability issues - const newResponse = new Response(res.body, res); - // Set cookie to enable persistent A/B sessions setCookie(c, NAME, group, { path: "/", - // Add additional cookie options as needed: - // secure: true, - // httpOnly: true, - // sameSite: 'strict', }); + } - // Copy the Set-Cookie header to the response - newResponse.headers.set( - "Set-Cookie", - c.res.headers.get("Set-Cookie") || "", - ); + const res = await fetch(url); - return newResponse; - } + return c.body(res.body, res); }); export default app; diff --git a/src/content/docs/workers/examples/accessing-the-cloudflare-object.mdx b/src/content/docs/workers/examples/accessing-the-cloudflare-object.mdx index 98c703c4c826acd..2c3d21d8708548f 100644 --- a/src/content/docs/workers/examples/accessing-the-cloudflare-object.mdx +++ b/src/content/docs/workers/examples/accessing-the-cloudflare-object.mdx @@ -61,9 +61,7 @@ export default { ```ts import { Hono } from "hono"; -type Bindings = {}; - -const app = new Hono<{ Bindings: Bindings }>(); +const app = new Hono(); app.get("*", async (c) => { // Access the raw request to get the cf object diff --git a/src/content/docs/workers/examples/aggregate-requests.mdx b/src/content/docs/workers/examples/aggregate-requests.mdx index 40aa95d9cdea3f4..4ca913c65272d2e 100644 --- a/src/content/docs/workers/examples/aggregate-requests.mdx +++ b/src/content/docs/workers/examples/aggregate-requests.mdx @@ -61,26 +61,24 @@ export default { ```ts -import { Hono } from 'hono'; - -type Bindings = {}; - -const app = new Hono<{ Bindings: Bindings }>(); - -app.get('*', async (c) => { - // someHost is set up to return JSON responses - const someHost = "https://jsonplaceholder.typicode.com"; - const url1 = someHost + "/todos/1"; - const url2 = someHost + "/todos/2"; - - // Fetch both URLs concurrently - const responses = await Promise.all([fetch(url1), fetch(url2)]); - - // Parse JSON responses concurrently - const results = await Promise.all(responses.map(r => r.json())); - - // Return aggregated results - return c.json(results); +import { Hono } from "hono"; + +const app = new Hono(); + +app.get("*", async (c) => { + // someHost is set up to return JSON responses + const someHost = "https://jsonplaceholder.typicode.com"; + const url1 = someHost + "/todos/1"; + const url2 = someHost + "/todos/2"; + + // Fetch both URLs concurrently + const responses = await Promise.all([fetch(url1), fetch(url2)]); + + // Parse JSON responses concurrently + const results = await Promise.all(responses.map((r) => r.json())); + + // Return aggregated results + return c.json(results); }); export default app; diff --git a/src/content/docs/workers/examples/basic-auth.mdx b/src/content/docs/workers/examples/basic-auth.mdx index 791a50082dfa2a1..0174f3fcdae0a8b 100644 --- a/src/content/docs/workers/examples/basic-auth.mdx +++ b/src/content/docs/workers/examples/basic-auth.mdx @@ -347,6 +347,7 @@ import { auth } from 'hono/basic-auth'; // Define environment interface interface Env { Bindings: { + USERNAME: string; PASSWORD: string; }; } @@ -358,20 +359,23 @@ app.get('/', (c) => { return c.text("Anyone can access the homepage."); }); -// Logout route -app.get('/logout', (c) => { - return c.text("Logged out.", 401); -}); - // Admin route - protected with Basic Auth -app.get('/admin', async (c, next) => { - return basicAuth({ username: c.env.USERNAME, password: c.env.PASSWORD })(c, next); -}); +app.get( + "/admin", + async (c, next) => { + const auth = basicAuth({ + username: c.env.USERNAME, + password: c.env.PASSWORD + }) + + return await auth(c, next); + }, + (c) => { + return c.text("🎉 You have private access!", 200, { + "Cache-Control": "no-store", + }); + } -// Handle 404 for any other routes -app.notFound((c) => { - return c.text("Not Found.", 404); -}); export default app; ```` diff --git a/src/content/docs/workers/examples/block-on-tls.mdx b/src/content/docs/workers/examples/block-on-tls.mdx index 5fd55f1c8f18214..b564e92c3c8e406 100644 --- a/src/content/docs/workers/examples/block-on-tls.mdx +++ b/src/content/docs/workers/examples/block-on-tls.mdx @@ -74,31 +74,28 @@ export default { ```ts import { Hono } from "hono"; -type Bindings = {}; - -const app = new Hono<{ Bindings: Bindings }>(); +const app = new Hono(); // Middleware to check TLS version app.use("*", async (c, next) => { - try { - // Access the raw request to get the cf object with TLS info - const request = c.req.raw; - const tlsVersion = request.cf?.tlsVersion; - - // Allow only TLS versions 1.2 and 1.3 - if (tlsVersion !== "TLSv1.2" && tlsVersion !== "TLSv1.3") { - return c.text("Please use TLS version 1.2 or higher.", 403); - } + // Access the raw request to get the cf object with TLS info + const request = c.req.raw; + const tlsVersion = request.cf?.tlsVersion; + + // Allow only TLS versions 1.2 and 1.3 + if (tlsVersion !== "TLSv1.2" && tlsVersion !== "TLSv1.3") { + return c.text("Please use TLS version 1.2 or higher.", 403); + } - // Continue to the next handler if TLS version is acceptable - return next(); - } catch (err: any) { - // Handle errors (especially in preview mode where request.cf might not exist) + await next(); + +}); + +app.onError((err, c) => { console.error( "request.cf does not exist in the previewer, only in production", ); return c.text(`Error in workers script: ${err.message}`, 500); - } }); app.get("/", async (c) => { diff --git a/src/content/docs/workers/examples/bulk-origin-proxy.mdx b/src/content/docs/workers/examples/bulk-origin-proxy.mdx index 51dc87f741465b2..d8b03e760233245 100644 --- a/src/content/docs/workers/examples/bulk-origin-proxy.mdx +++ b/src/content/docs/workers/examples/bulk-origin-proxy.mdx @@ -101,7 +101,7 @@ app.all("*", async (c) => { } // Otherwise, process request as normal - return fetch(c.req.raw); + return proxy(c.req.raw); }); export default app; diff --git a/src/content/docs/workers/examples/bulk-redirects.mdx b/src/content/docs/workers/examples/bulk-redirects.mdx index 9651a1937882f92..8f5e60ed41ce114 100644 --- a/src/content/docs/workers/examples/bulk-redirects.mdx +++ b/src/content/docs/workers/examples/bulk-redirects.mdx @@ -102,7 +102,7 @@ async def on_fetch(request): ```ts -import { Hono } from 'hono'; +import { Hono } from "hono"; const app = new Hono(); @@ -110,30 +110,30 @@ const app = new Hono(); const externalHostname = "examples.cloudflareworkers.com"; const redirectMap = new Map([ - ["/bulk1", `https://${externalHostname}/redirect2`], - ["/bulk2", `https://${externalHostname}/redirect3`], - ["/bulk3", `https://${externalHostname}/redirect4`], - ["/bulk4", "https://google.com"], + ["/bulk1", `https://${externalHostname}/redirect2`], + ["/bulk2", `https://${externalHostname}/redirect3`], + ["/bulk3", `https://${externalHostname}/redirect4`], + ["/bulk4", "https://google.com"], ]); // Middleware to handle redirects -app.use('*', async (c, next) => { - const path = new URL(c.req.url).pathname; - const location = redirectMap.get(path); +app.use("*", async (c, next) => { + const path = c.req.path; + const location = redirectMap.get(path); - if (location) { - // If path is in our redirect map, perform the redirect - return c.redirect(location, 301); - } + if (location) { + // If path is in our redirect map, perform the redirect + return c.redirect(location, 301); + } - // Otherwise, continue to the next handler - await next(); + // Otherwise, continue to the next handler + await next(); }); // Default handler for requests that don't match any redirects -app.all('*', async (c) => { - // Pass through to origin - return fetch(c.req.raw); +app.all("*", async (c) => { + // Pass through to origin + return fetch(c.req.raw); }); export default app; diff --git a/src/content/docs/workers/examples/cache-api.mdx b/src/content/docs/workers/examples/cache-api.mdx index 3bfb8e52d78a163..3082c2749c05cad 100644 --- a/src/content/docs/workers/examples/cache-api.mdx +++ b/src/content/docs/workers/examples/cache-api.mdx @@ -136,13 +136,8 @@ async def on_fetch(request, _env, ctx): ```ts import { Hono } from "hono"; -import type { Context } from "hono"; import { cache } from "hono/cache"; -type Env = { - // Cloudflare Worker environment -}; - const app = new Hono(); // We leverage hono built-in cache helper here diff --git a/src/content/docs/workers/examples/geolocation-hello-world.mdx b/src/content/docs/workers/examples/geolocation-hello-world.mdx index aed295bc3badae0..3813dcc67e2eac3 100644 --- a/src/content/docs/workers/examples/geolocation-hello-world.mdx +++ b/src/content/docs/workers/examples/geolocation-hello-world.mdx @@ -166,7 +166,7 @@ const app = new Hono(); app.get("*", (c) => { // Cast the raw request to include Cloudflare-specific properties - const request = c.req.raw as RequestWithCf; + const request = c.req.raw; // Define styles const html_style = diff --git a/src/content/docs/workers/examples/logging-headers.mdx b/src/content/docs/workers/examples/logging-headers.mdx index ab634ea220660ed..04e84e90fd5a175 100644 --- a/src/content/docs/workers/examples/logging-headers.mdx +++ b/src/content/docs/workers/examples/logging-headers.mdx @@ -57,11 +57,12 @@ async def on_fetch(request): use worker::*; #[event(fetch)] -async fn fetch(req: HttpRequest, _env: Env, _ctx: Context) -> Result { - console_log!("{:?}", req.headers()); - Response::ok("hello world") +async fn fetch(req: HttpRequest, \_env: Env, \_ctx: Context) -> Result { +console_log!("{:?}", req.headers()); +Response::ok("hello world") } -``` + +```` ```ts @@ -71,27 +72,27 @@ const app = new Hono(); app.get('*', (c) => { // Different ways to log headers in Hono: - + // 1. Using Map to display headers in console console.log('Headers as Map:', new Map(c.req.raw.headers)); - + // 2. Using spread operator to log headers console.log('Headers spread:', [...c.req.raw.headers]); - + // 3. Using Object.fromEntries to convert to an object console.log('Headers as Object:', Object.fromEntries(c.req.raw.headers)); - + // 4. Hono's built-in header accessor (for individual headers) console.log('User-Agent:', c.req.header('User-Agent')); - + // 5. Using c.req.headers to get all headers - console.log('All headers from Hono context:', c.req.headers); - + console.log('All headers from Hono context:', c.req.header()); + return c.text('Hello world'); }); export default app; -``` +```` @@ -184,4 +185,4 @@ Request headers: { "cf-ipcountry": "US", // ... }" -``` \ No newline at end of file +``` diff --git a/src/content/docs/workers/examples/modify-request-property.mdx b/src/content/docs/workers/examples/modify-request-property.mdx index 74f08e72a9dfe7f..13a888c1296fddc 100644 --- a/src/content/docs/workers/examples/modify-request-property.mdx +++ b/src/content/docs/workers/examples/modify-request-property.mdx @@ -187,58 +187,52 @@ async def on_fetch(request): ```ts -import { Hono } from 'hono'; +import { Hono } from "hono"; const app = new Hono(); -app.all('*', async (c) => { - /** - * Example someHost is set up to return raw JSON - */ - const someHost = "example.com"; - const someUrl = "https://foo.example.com/api.js"; - - try { - // Create a URL object to modify the hostname - const url = new URL(someUrl); - url.hostname = someHost; - - // In Hono, we can create a new request with modified properties - - // Define the new request properties - const newRequestInit = { - // Change method - method: "POST", - // Change body - body: JSON.stringify({ bar: "foo" }), - // Change the redirect mode - redirect: "follow" as RequestRedirect, - // Change headers, note this method will erase existing headers - headers: { - "Content-Type": "application/json", - "X-Example": "bar" - }, - // Change a Cloudflare feature on the outbound response - cf: { apps: false }, - }; - - // Create a new request - // First create a clone of the original request with the new properties - const requestClone = new Request(c.req.raw, newRequestInit); - - // Then create a new request with the modified URL - const newRequest = new Request(url.toString(), requestClone); - - // Send the modified request - const response = await fetch(newRequest); - - // Return the response - return response; - - } catch (e) { - // Handle errors - return c.json({ error: e.message }, 500); - } +app.all("*", async (c) => { + /** + * Example someHost is set up to return raw JSON + */ + const someHost = "example.com"; + const someUrl = "https://foo.example.com/api.js"; + + // Create a URL object to modify the hostname + const url = new URL(someUrl); + url.hostname = someHost; + + // Create a new request + // First create a clone of the original request with the new properties + const requestClone = new Request(c.req.raw, { + // Change method + method: "POST", + // Change body + body: JSON.stringify({ bar: "foo" }), + // Change the redirect mode + redirect: "follow" as RequestRedirect, + // Change headers, note this method will erase existing headers + headers: { + "Content-Type": "application/json", + "X-Example": "bar", + }, + // Change a Cloudflare feature on the outbound response + cf: { apps: false }, + }); + + // Then create a new request with the modified URL + const newRequest = new Request(url.toString(), requestClone); + + // Send the modified request + const response = await fetch(newRequest); + + // Return the response + return response; +}); + +// Handle errors +app.onError((err, c) => { + return err.getResponse(); }); export default app; diff --git a/src/content/docs/workers/examples/signing-requests.mdx b/src/content/docs/workers/examples/signing-requests.mdx index d511a27c31cd12f..ebbd9b5c57d68d4 100644 --- a/src/content/docs/workers/examples/signing-requests.mdx +++ b/src/content/docs/workers/examples/signing-requests.mdx @@ -307,7 +307,7 @@ app.get("/generate/*", async (c) => { // Handle verification for all other requests app.all("*", async (c) => { const env = c.env; - const url = c.req.url) + const url = c.req.url; // You will need some secret data to use as a symmetric key const secretKeyData = encoder.encode( diff --git a/src/content/docs/workers/examples/turnstile-html-rewriter.mdx b/src/content/docs/workers/examples/turnstile-html-rewriter.mdx index 5d9f048ffdb7e41..f3dbf28c007b536 100644 --- a/src/content/docs/workers/examples/turnstile-html-rewriter.mdx +++ b/src/content/docs/workers/examples/turnstile-html-rewriter.mdx @@ -98,112 +98,112 @@ export default { ```ts -import { Hono } from 'hono'; -import { html } from 'hono/html'; +import { Hono } from "hono"; interface Env { - SITE_KEY: string; - TURNSTILE_ATTR_NAME?: string; + SITE_KEY: string; + SECRET_KEY: string; + TURNSTILE_ATTR_NAME?: string; } const app = new Hono<{ Bindings: Env }>(); // Middleware to inject Turnstile widget -app.use('*', async (c, next) => { - const SITE_KEY = c.env.SITE_KEY; // The Turnstile Sitekey from environment - const TURNSTILE_ATTR_NAME = c.env.TURNSTILE_ATTR_NAME || "your_id_to_replace"; // The target element ID - - // Process the request through the original endpoint - await next(); - - // Only process HTML responses - const contentType = c.res.headers.get('content-type'); - if (!contentType || !contentType.includes('text/html')) { - return; - } - - // Clone the response to make it modifiable - const originalResponse = c.res; - const responseBody = await originalResponse.text(); - - // Create an HTMLRewriter instance to modify the HTML - const rewriter = new HTMLRewriter() - // Add the Turnstile script to the head - .on('head', { - element(element) { - element.append( - ``, - { html: true } - ); - } - }) - // Add the Turnstile widget to the target div - .on('div', { - element(element) { - if (element.getAttribute('id') === TURNSTILE_ATTR_NAME) { - element.append( - `

`, - { html: true } - ); - } - } - }); - - // Create a new response with the same properties as the original - const modifiedResponse = new Response(responseBody, { - status: originalResponse.status, - statusText: originalResponse.statusText, - headers: originalResponse.headers - }); - - // Transform the response using HTMLRewriter - c.res = rewriter.transform(modifiedResponse); +app.use("*", async (c, next) => { + const SITE_KEY = c.env.SITE_KEY; // The Turnstile Sitekey from environment + const TURNSTILE_ATTR_NAME = c.env.TURNSTILE_ATTR_NAME || "your_id_to_replace"; // The target element ID + + // Process the request through the original endpoint + await next(); + + // Only process HTML responses + const contentType = c.res.headers.get("content-type"); + if (!contentType || !contentType.includes("text/html")) { + return; + } + + // Clone the response to make it modifiable + const originalResponse = c.res; + const responseBody = await originalResponse.text(); + + // Create an HTMLRewriter instance to modify the HTML + const rewriter = new HTMLRewriter() + // Add the Turnstile script to the head + .on("head", { + element(element) { + element.append( + ``, + { html: true }, + ); + }, + }) + // Add the Turnstile widget to the target div + .on("div", { + element(element) { + if (element.getAttribute("id") === TURNSTILE_ATTR_NAME) { + element.append( + `
`, + { html: true }, + ); + } + }, + }); + + // Create a new response with the same properties as the original + const modifiedResponse = new Response(responseBody, { + status: originalResponse.status, + statusText: originalResponse.statusText, + headers: originalResponse.headers, + }); + + // Transform the response using HTMLRewriter + c.res = rewriter.transform(modifiedResponse); }); // Handle POST requests for form submission with Turnstile validation -app.post('*', async (c) => { - const formData = await c.req.formData(); - const token = formData.get('cf-turnstile-response'); - const ip = c.req.header('CF-Connecting-IP'); - - // If no token, return an error - if (!token) { - return c.text('Missing Turnstile token', 400); - } - - // Prepare verification data - const verifyFormData = new FormData(); - verifyFormData.append('secret', c.env.SECRET_KEY || ''); - verifyFormData.append('response', token.toString()); - if (ip) verifyFormData.append('remoteip', ip); - - // Verify the token with Turnstile API - const verifyResult = await fetch( - 'https://challenges.cloudflare.com/turnstile/v0/siteverify', - { - method: 'POST', - body: verifyFormData - } - ); - - const outcome = await verifyResult.json(); - - // If verification fails, return an error - if (!outcome.success) { - return c.text('The provided Turnstile token was not valid!', 401); - } - - // If verification succeeds, proceed with the original request - // You would typically handle the form submission logic here - - // For this example, we'll just send a success response - return c.text('Form submission successful!'); +app.post("*", async (c) => { + const formData = await c.req.formData(); + const token = formData.get("cf-turnstile-response"); + const ip = c.req.header("CF-Connecting-IP"); + + // If no token, return an error + if (!token) { + return c.text("Missing Turnstile token", 400); + } + + // Prepare verification data + const verifyFormData = new FormData(); + verifyFormData.append("secret", c.env.SECRET_KEY || ""); + verifyFormData.append("response", token.toString()); + if (ip) verifyFormData.append("remoteip", ip); + + // Verify the token with Turnstile API + const verifyResult = await fetch( + "https://challenges.cloudflare.com/turnstile/v0/siteverify", + { + method: "POST", + body: verifyFormData, + }, + ); + + const outcome = await verifyResult.json<{ success: boolean }>; + + // If verification fails, return an error + if (!outcome.success) { + return c.text("The provided Turnstile token was not valid!", 401); + } + + // If verification succeeds, proceed with the original request + // You would typically handle the form submission logic here + + // For this example, we'll just send a success response + return c.text("Form submission successful!"); }); // Default handler for GET requests -app.get('*', async (c) => { - // Fetch the original content (you'd replace this with your actual content source) - return await fetch(c.req.raw); +app.get("*", async (c) => { + // Fetch the original content (you'd replace this with your actual content source) + return await fetch(c.req.raw); }); export default app; From 6cdc391e7c8861e279a3fb40f5ae8f8480ab9f70 Mon Sep 17 00:00:00 2001 From: Lucas Kostka Date: Mon, 14 Apr 2025 15:46:31 +0200 Subject: [PATCH 5/5] Fixed hono example for basic auth --- src/content/docs/workers/examples/basic-auth.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/content/docs/workers/examples/basic-auth.mdx b/src/content/docs/workers/examples/basic-auth.mdx index 0174f3fcdae0a8b..46105a13575d8a7 100644 --- a/src/content/docs/workers/examples/basic-auth.mdx +++ b/src/content/docs/workers/examples/basic-auth.mdx @@ -341,8 +341,8 @@ let url = req.url()?; * @see https://tools.ietf.org/html/rfc7617 */ -import { Hono } from 'hono'; -import { auth } from 'hono/basic-auth'; +import { Hono } from "hono"; +import { basicAuth } from "hono/basic-auth"; // Define environment interface interface Env { @@ -355,7 +355,7 @@ interface Env { const app = new Hono(); // Public homepage - accessible to everyone -app.get('/', (c) => { +app.get("/", (c) => { return c.text("Anyone can access the homepage."); }); @@ -375,7 +375,7 @@ app.get( "Cache-Control": "no-store", }); } - +); export default app; ````