diff --git a/src/content/docs/workers/examples/103-early-hints.mdx b/src/content/docs/workers/examples/103-early-hints.mdx index adcae028ab00206..4753b6f032471e7 100644 --- a/src/content/docs/workers/examples/103-early-hints.mdx +++ b/src/content/docs/workers/examples/103-early-hints.mdx @@ -136,4 +136,47 @@ 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.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 3b71cca1afaeadf..1a4807ba6b7f7dc 100644 --- a/src/content/docs/workers/examples/ab-testing.mdx +++ b/src/content/docs/workers/examples/ab-testing.mdx @@ -137,4 +137,57 @@ 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"; + +// 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) => { + 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 + url.pathname = "/control" + c.req.path; + } else if (abTestCookie === "test") { + // User is in test group + 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) + const group = Math.random() < 0.5 ? "test" : "control"; + + // Update URL path based on assigned group + if (group === "control") { + url.pathname = "/control" + c.req.path; + } else { + url.pathname = "/test" + c.req.path; + } + + // Set cookie to enable persistent A/B sessions + setCookie(c, NAME, group, { + path: "/", + }); + } + + const res = await fetch(url); + + 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 47039f0e893ec93..2c3d21d8708548f 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,30 @@ export default { } satisfies ExportedHandler; ``` + + +```ts +import { Hono } from "hono"; + +const app = new Hono(); + +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..4ca913c65272d2e 100644 --- a/src/content/docs/workers/examples/aggregate-requests.mdx +++ b/src/content/docs/workers/examples/aggregate-requests.mdx @@ -58,6 +58,32 @@ export default { } satisfies ExportedHandler; ``` + + +```ts +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; +``` + ```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..46105a13575d8a7 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,6 +328,56 @@ async fn fetch(req: Request, env: Env, _ctx: Context) -> Result { } _ => Response::error("Not Found.", 404), } + } -``` - \ No newline at end of file + +```` + + +```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 { basicAuth } from "hono/basic-auth"; + +// Define environment interface +interface Env { + Bindings: { + USERNAME: string; + PASSWORD: string; + }; +} + +const app = new Hono(); + +// Public homepage - accessible to everyone +app.get("/", (c) => { + return c.text("Anyone can access the homepage."); +}); + +// Admin route - protected with Basic Auth +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", + }); + } +); + +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 05741afacb2fb6c..b564e92c3c8e406 100644 --- a/src/content/docs/workers/examples/block-on-tls.mdx +++ b/src/content/docs/workers/examples/block-on-tls.mdx @@ -69,6 +69,42 @@ export default { } satisfies ExportedHandler; ``` + + +```ts +import { Hono } from "hono"; + +const app = new Hono(); + +// Middleware to check TLS version +app.use("*", async (c, next) => { + // 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); + } + + 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) => { + 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..d8b03e760233245 100644 --- a/src/content/docs/workers/examples/bulk-origin-proxy.mdx +++ b/src/content/docs/workers/examples/bulk-origin-proxy.mdx @@ -74,6 +74,39 @@ export default { } satisfies ExportedHandler; ``` + + +```ts +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", +}; + +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 proxy(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..8f5e60ed41ce114 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 = 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); + } + + // 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..3082c2749c05cad 100644 --- a/src/content/docs/workers/examples/cache-api.mdx +++ b/src/content/docs/workers/examples/cache-api.mdx @@ -132,4 +132,29 @@ async def on_fetch(request, _env, ctx): return response ``` + + +```ts +import { Hono } from "hono"; +import { cache } from "hono/cache"; + +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!"); +}); + +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..725722ea28d0763 100644 --- a/src/content/docs/workers/examples/cache-post-request.mdx +++ b/src/content/docs/workers/examples/cache-post-request.mdx @@ -147,4 +147,58 @@ async def on_fetch(request, _, ctx): return fetch(request) ``` + + +```ts +import { Hono } from "hono"; +import { sha256 } from "hono/utils/crypto"; + +const app = new Hono(); + +// 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..0b0993baf4458c3 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,46 @@ export default { } satisfies ExportedHandler; ``` + + +```ts +import { Hono } from "hono"; + +const app = new Hono(); + +app.all("*", async (c) => { + const tags = c.req.query("tags") ? c.req.query("tags").split(",") : []; + const uri = c.req.query("uri") ? c.req.query("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); +}); + +app.onError((err, c) => { + 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..1295423b4e000c9 100644 --- a/src/content/docs/workers/examples/conditional-response.mdx +++ b/src/content/docs/workers/examples/conditional-response.mdx @@ -163,4 +163,76 @@ async def on_fetch(request): return fetch(request) ``` + + +```ts +import { Hono } from "hono"; +import { HTTPException } from "hono/http-exception"; + +const app = new Hono(); + +// Middleware to handle all conditions before reaching the main handler +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"); +}); + +// 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..e25baa21e3c8f57 100644 --- a/src/content/docs/workers/examples/cors-header-proxy.mdx +++ b/src/content/docs/workers/examples/cors-header-proxy.mdx @@ -328,6 +328,145 @@ export default { } satisfies ExportedHandler; ``` + + +```ts +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 +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(); + +// Demo page handler +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 = ` + + + +

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 @@ -442,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() { @@ -559,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/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 1e724f454d6fa27..fbfdd541d4f5e22 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..3813dcc67e2eac3 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; + + // 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..04e84e90fd5a175 100644 --- a/src/content/docs/workers/examples/logging-headers.mdx +++ b/src/content/docs/workers/examples/logging-headers.mdx @@ -57,11 +57,43 @@ 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 +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.header()); + + return c.text('Hello world'); +}); + +export default app; +```` + --- @@ -153,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 0bf3856c4615d47..13a888c1296fddc 100644 --- a/src/content/docs/workers/examples/modify-request-property.mdx +++ b/src/content/docs/workers/examples/modify-request-property.mdx @@ -184,4 +184,58 @@ 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"; + + // 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/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..76ae5af6accdfc3 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,40 @@ export default { }, } satisfies ExportedHandler; ``` + + + +```ts +import { Hono } from "hono"; +import { streamText } from "hono/streaming"; +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, + }); + + 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/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 086ace6f5d277e7..9b0a42dad691d6f 100644 --- a/src/content/docs/workers/examples/security-headers.mdx +++ b/src/content/docs/workers/examples/security-headers.mdx @@ -320,6 +320,25 @@ 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())) + } -``` - \ No newline at end of file + +```` + + +```ts +import { Hono } from 'hono'; +import { secureHeaders } from 'hono/secure-headers'; + +const app = new Hono(); +app.use(secureHeaders()); + +// Handle all other 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/signing-requests.mdx b/src/content/docs/workers/examples/signing-requests.mdx index e4706525d09d569..ebbd9b5c57d68d4 100644 --- a/src/content/docs/workers/examples/signing-requests.mdx +++ b/src/content/docs/workers/examples/signing-requests.mdx @@ -244,6 +244,213 @@ export default { } satisfies ExportedHandler; ``` + + +```ts +import { Buffer } from "node:buffer"; +import { Hono } from "hono"; +import { proxy } from "hono/proxy"; + +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(); + +// 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"], + ); + + // 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 = 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; +``` + + + +```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..f3dbf28c007b536 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"; + +interface Env { + 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); +}); + +// 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<{ 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); +}); + +export default app; +``` +
```py diff --git a/src/content/docs/workers/examples/websockets.mdx b/src/content/docs/workers/examples/websockets.mdx index 3cdaba73ca77682..c883e2641e32fbc 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/Reference/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,9 +233,39 @@ async fn fetch(req: HttpRequest, _env: Env, _ctx: Context) -> Result + +```ts +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; +```` + ### Connect to the WebSocket server from a client @@ -241,17 +273,19 @@ async fn fetch(req: HttpRequest, _env: Env, _ctx: Context) -> Result { - 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): @@ -270,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.