diff --git a/src/content/changelog/workers/2025-05-14-python-worker-durable-object.mdx b/src/content/changelog/workers/2025-05-14-python-worker-durable-object.mdx index 0ac40bc24c22e0c..0467f5159f0f0fe 100644 --- a/src/content/changelog/workers/2025-05-14-python-worker-durable-object.mdx +++ b/src/content/changelog/workers/2025-05-14-python-worker-durable-object.mdx @@ -18,7 +18,7 @@ long-running applications which run close to your users. For more info see You can define a Durable Object in Python in a similar way to JavaScript: ```python -from workers import DurableObject, Response, handler +from workers import DurableObject, Response, WorkerEntrypoint from urllib.parse import urlparse @@ -27,17 +27,17 @@ class MyDurableObject(DurableObject): self.ctx = ctx self.env = env - def on_fetch(self, request): + def fetch(self, request): result = self.ctx.storage.sql.exec("SELECT 'Hello, World!' as greeting").one() return Response(result.greeting) -@handler -async def on_fetch(request, env, ctx): - url = urlparse(request.url) - id = env.MY_DURABLE_OBJECT.idFromName(url.path) - stub = env.MY_DURABLE_OBJECT.get(id) - greeting = await stub.fetch(request.url) - return greeting +class Default(WorkerEntrypoint): + async def fetch(self, request, env, ctx): + url = urlparse(request.url) + id = env.MY_DURABLE_OBJECT.idFromName(url.path) + stub = env.MY_DURABLE_OBJECT.get(id) + greeting = await stub.fetch(request.url) + return greeting ``` Define the Durable Object in your Wrangler configuration file: diff --git a/src/content/changelog/workers/2025-08-14-new-python-handlers.mdx b/src/content/changelog/workers/2025-08-14-new-python-handlers.mdx new file mode 100644 index 000000000000000..e4e13e561790838 --- /dev/null +++ b/src/content/changelog/workers/2025-08-14-new-python-handlers.mdx @@ -0,0 +1,33 @@ +--- +title: Python Workers handlers now live in an entrypoint class +description: We are changing how Python Workers are structured by default. +products: + - workers +date: 2025-08-14 +--- + +import { WranglerConfig } from "~/components"; + +We are changing how Python Workers are structured by default. Previously, handlers were defined at the top-level of a module as `on_fetch`, `on_scheduled`, etc. methods, but now they live in an entrypoint class. + +Here's an example of how to now define a Worker with a fetch handler: + +```python +from workers import Response, WorkerEntrypoint + +class Default(WorkerEntrypoint): + async def fetch(self, request, env, ctx): + return Response("Hello World!") +``` + +To keep using the old-style handlers, you can specify the `disable_python_no_global_handlers` compatibility flag in your wrangler file: + + + +```toml +compatibility_flags = [ "disable_python_no_global_handlers" ] +``` + + + +Consult the [Python Workers documentation](/workers/languages/python/) for more details. \ No newline at end of file diff --git a/src/content/docs/d1/examples/query-d1-from-python-workers.mdx b/src/content/docs/d1/examples/query-d1-from-python-workers.mdx index 59165521c4141e2..cb87c543c31d0b9 100644 --- a/src/content/docs/d1/examples/query-d1-from-python-workers.mdx +++ b/src/content/docs/d1/examples/query-d1-from-python-workers.mdx @@ -81,15 +81,16 @@ The value of `binding` is how you will refer to your database from within your W To create a Python Worker, create an empty file at `src/entry.py`, matching the value of `main` in your Wrangler file with the contents below: ```python -from workers import Response +from workers import Response, WorkerEntrypoint -async def on_fetch(request, env): - # Do anything else you'd like on request here! +class Default(WorkerEntrypoint): + async def fetch(self, request, env): + # Do anything else you'd like on request here! - # Query D1 - we'll list all tables in our database in this example - results = await env.DB.prepare("PRAGMA table_list").run() - # Return a JSON response - return Response.json(results) + # Query D1 - we'll list all tables in our database in this example + results = await env.DB.prepare("PRAGMA table_list").run() + # Return a JSON response + return Response.json(results) ``` diff --git a/src/content/docs/durable-objects/get-started.mdx b/src/content/docs/durable-objects/get-started.mdx index 8d307aa9afda65c..1d6e98390a63de8 100644 --- a/src/content/docs/durable-objects/get-started.mdx +++ b/src/content/docs/durable-objects/get-started.mdx @@ -185,16 +185,16 @@ export default { ```python -from workers import handler, Response +from workers import handler, Response, WorkerEntrypoint from urllib.parse import urlparse -@handler -async def on_fetch(request, env, ctx): - url = urlparse(request.url) - id = env.MY_DURABLE_OBJECT.idFromName(url.path) - stub = env.MY_DURABLE_OBJECT.get(id) - greeting = await stub.say_hello() - return Response(greeting) +class Default(WorkerEntrypoint): + async def fetch(request, env, ctx): + url = urlparse(request.url) + id = env.MY_DURABLE_OBJECT.idFromName(url.path) + stub = env.MY_DURABLE_OBJECT.get(id) + greeting = await stub.say_hello() + return Response(greeting) ``` @@ -328,13 +328,13 @@ class MyDurableObject(DurableObject): return result.greeting -@handler -async def on_fetch(request, env, ctx): - url = urlparse(request.url) - id = env.MY_DURABLE_OBJECT.idFromName(url.path) - stub = env.MY_DURABLE_OBJECT.get(id) - greeting = await stub.say_hello() - return Response(greeting) +class Default(WorkerEntrypoint): + async def fetch(self, request, env, ctx): + url = urlparse(request.url) + id = env.MY_DURABLE_OBJECT.idFromName(url.path) + stub = env.MY_DURABLE_OBJECT.get(id) + greeting = await stub.say_hello() + return Response(greeting) ``` diff --git a/src/content/docs/workers/examples/103-early-hints.mdx b/src/content/docs/workers/examples/103-early-hints.mdx index 471ed7302b38c9c..ec14e6fa12d05de 100644 --- a/src/content/docs/workers/examples/103-early-hints.mdx +++ b/src/content/docs/workers/examples/103-early-hints.mdx @@ -116,7 +116,7 @@ export default { ```py import re -from workers import Response +from workers import Response, WorkerEntrypoint CSS = "body { color: red; }" HTML = """ @@ -132,12 +132,14 @@ HTML = """ """ -def on_fetch(request): - if re.search("test.css", request.url): - headers = {"content-type": "text/css"} - return Response(CSS, headers=headers) - else: - headers = {"content-type": "text/html","link": "; rel=preload; as=style"} + +class Default(WorkerEntrypoint): + async def fetch(self, request): + if re.search("test.css", request.url): + headers = {"content-type": "text/css"} + return Response(CSS, headers=headers) + else: + headers = {"content-type": "text/html","link": "; rel=preload; as=style"} return Response(HTML, headers=headers) ``` diff --git a/src/content/docs/workers/examples/ab-testing.mdx b/src/content/docs/workers/examples/ab-testing.mdx index 154011e16604e63..576548c593995ef 100644 --- a/src/content/docs/workers/examples/ab-testing.mdx +++ b/src/content/docs/workers/examples/ab-testing.mdx @@ -100,41 +100,42 @@ export default { ```py import random from urllib.parse import urlparse, urlunparse -from workers import Response, fetch +from workers import Response, fetch, WorkerEntrypoint NAME = "myExampleWorkersABTest" -async def on_fetch(request): - url = urlparse(request.url) - # Uncomment below when testing locally - # url = url._replace(netloc="example.com") if "localhost" in url.netloc else url - - # Enable Passthrough to allow direct access to control and test routes. - if url.path.startswith("/control") or url.path.startswith("/test"): - return fetch(urlunparse(url)) - - # Determine which group this requester is in. - cookie = request.headers.get("cookie") - - if cookie and f'{NAME}=control' in cookie: - url = url._replace(path="/control" + url.path) - elif cookie and f'{NAME}=test' in cookie: - url = url._replace(path="/test" + url.path) - else: - # If there is no cookie, this is a new client. Choose a group and set the cookie. - group = "test" if random.random() < 0.5 else "control" - if group == "control": - url = url._replace(path="/control" + url.path) - else: - url = url._replace(path="/test" + url.path) - - # Reconstruct response to avoid immutability - res = await fetch(urlunparse(url)) - headers = dict(res.headers) - headers["Set-Cookie"] = f'{NAME}={group}; path=/' - return Response(res.body, headers=headers) - - return fetch(urlunparse(url)) +class Default(WorkerEntrypoint): + async def fetch(self, request): + url = urlparse(request.url) + # Uncomment below when testing locally + # url = url._replace(netloc="example.com") if "localhost" in url.netloc else url + + # Enable Passthrough to allow direct access to control and test routes. + if url.path.startswith("/control") or url.path.startswith("/test"): + return fetch(urlunparse(url)) + + # Determine which group this requester is in. + cookie = request.headers.get("cookie") + + if cookie and f'{NAME}=control' in cookie: + url = url._replace(path="/control" + url.path) + elif cookie and f'{NAME}=test' in cookie: + url = url._replace(path="/test" + url.path) + else: + # If there is no cookie, this is a new client. Choose a group and set the cookie. + group = "test" if random.random() < 0.5 else "control" + if group == "control": + url = url._replace(path="/control" + url.path) + else: + url = url._replace(path="/test" + url.path) + + # Reconstruct response to avoid immutability + res = await fetch(urlunparse(url)) + headers = dict(res.headers) + headers["Set-Cookie"] = f'{NAME}={group}; path=/' + return Response(res.body, headers=headers) + + return fetch(urlunparse(url)) ``` 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 6008379b872ebb2..70298fc575db2b9 100644 --- a/src/content/docs/workers/examples/accessing-the-cloudflare-object.mdx +++ b/src/content/docs/workers/examples/accessing-the-cloudflare-object.mdx @@ -90,14 +90,15 @@ export default app; ```py import json -from workers import Response +from workers import Response, WorkerEntrypoint from js import JSON -def on_fetch(request): - error = json.dumps({ "error": "The `cf` object is not available inside the preview." }) - data = request.cf if request.cf is not None else error - headers = {"content-type":"application/json"} - return Response(JSON.stringify(data, None, 2), headers=headers) +class Default(WorkerEntrypoint): + async def fetch(self, request): + error = json.dumps({ "error": "The `cf` object is not available inside the preview." }) + data = request.cf if request.cf is not None else error + headers = {"content-type":"application/json"} + return Response(JSON.stringify(data, None, 2), headers=headers) ``` diff --git a/src/content/docs/workers/examples/aggregate-requests.mdx b/src/content/docs/workers/examples/aggregate-requests.mdx index 428955965db5dea..63f630320af7c74 100644 --- a/src/content/docs/workers/examples/aggregate-requests.mdx +++ b/src/content/docs/workers/examples/aggregate-requests.mdx @@ -93,21 +93,22 @@ export default app; ```py -from workers import Response, fetch +from workers import Response, fetch, WorkerEntrypoint import asyncio import json -async def on_fetch(request): - # some_host is set up to return JSON responses - some_host = "https://jsonplaceholder.typicode.com" - url1 = some_host + "/todos/1" - url2 = some_host + "/todos/2" +class Default(WorkerEntrypoint): + async def fetch(self, request): + # some_host is set up to return JSON responses + some_host = "https://jsonplaceholder.typicode.com" + url1 = some_host + "/todos/1" + url2 = some_host + "/todos/2" - responses = await asyncio.gather(fetch(url1), fetch(url2)) - results = await asyncio.gather(*(r.json() for r in responses)) + responses = await asyncio.gather(fetch(url1), fetch(url2)) + results = await asyncio.gather(*(r.json() for r in responses)) - headers = {"content-type": "application/json;charset=UTF-8"} - return Response.json(results, headers=headers) + headers = {"content-type": "application/json;charset=UTF-8"} + return Response.json(results, headers=headers) ``` diff --git a/src/content/docs/workers/examples/alter-headers.mdx b/src/content/docs/workers/examples/alter-headers.mdx index 0fd99a28d27c1d9..5b6404efe09c304 100644 --- a/src/content/docs/workers/examples/alter-headers.mdx +++ b/src/content/docs/workers/examples/alter-headers.mdx @@ -85,27 +85,28 @@ export default { ```py -from workers import Response, fetch +from workers import Response, fetch, WorkerEntrypoint -async def on_fetch(request): - response = await fetch("https://example.com") +class Default(WorkerEntrypoint): + async def fetch(self, request): + response = await fetch("https://example.com") - # Grab the response headers so they can be modified - new_headers = response.headers + # Grab the response headers so they can be modified + new_headers = response.headers - # Add a custom header with a value - new_headers["x-workers-hello"] = "Hello from Cloudflare Workers" + # Add a custom header with a value + new_headers["x-workers-hello"] = "Hello from Cloudflare Workers" - # Delete headers - if "x-header-to-delete" in new_headers: - del new_headers["x-header-to-delete"] - if "x-header2-to-delete" in new_headers: - del new_headers["x-header2-to-delete"] + # Delete headers + if "x-header-to-delete" in new_headers: + del new_headers["x-header-to-delete"] + if "x-header2-to-delete" in new_headers: + del new_headers["x-header2-to-delete"] - # Adjust the value for an existing header - new_headers["x-header-to-change"] = "NewValue" + # Adjust the value for an existing header + new_headers["x-header-to-change"] = "NewValue" - return Response(response.body, headers=new_headers) + return Response(response.body, headers=new_headers) ``` diff --git a/src/content/docs/workers/examples/auth-with-headers.mdx b/src/content/docs/workers/examples/auth-with-headers.mdx index ae47f78a398f165..da97846e3079474 100644 --- a/src/content/docs/workers/examples/auth-with-headers.mdx +++ b/src/content/docs/workers/examples/auth-with-headers.mdx @@ -78,20 +78,21 @@ export default { ```py -from workers import Response, fetch +from workers import WorkerEntrypoint, Response, fetch -async def on_fetch(request): - PRESHARED_AUTH_HEADER_KEY = "X-Custom-PSK" - PRESHARED_AUTH_HEADER_VALUE = "mypresharedkey" +class Default(WorkerEntrypoint): + async def fetch(self, request): + PRESHARED_AUTH_HEADER_KEY = "X-Custom-PSK" + PRESHARED_AUTH_HEADER_VALUE = "mypresharedkey" - psk = request.headers[PRESHARED_AUTH_HEADER_KEY] + psk = request.headers[PRESHARED_AUTH_HEADER_KEY] - if psk == PRESHARED_AUTH_HEADER_VALUE: - # Correct preshared header key supplied. Fetch request from origin. - return fetch(request) + if psk == PRESHARED_AUTH_HEADER_VALUE: + # Correct preshared header key supplied. Fetch request from origin. + return fetch(request) - # Incorrect key supplied. Reject the request. - return Response("Sorry, you have supplied an invalid key.", status=403) + # Incorrect key supplied. Reject the request. + return Response("Sorry, you have supplied an invalid key.", status=403) ``` diff --git a/src/content/docs/workers/examples/block-on-tls.mdx b/src/content/docs/workers/examples/block-on-tls.mdx index b340f8fe6cae7b2..3e477c136ddc20e 100644 --- a/src/content/docs/workers/examples/block-on-tls.mdx +++ b/src/content/docs/workers/examples/block-on-tls.mdx @@ -107,13 +107,14 @@ export default app; ```py -from workers import Response, fetch - -async def on_fetch(request): - tls_version = request.cf.tlsVersion - if tls_version not in ("TLSv1.2", "TLSv1.3"): - return Response("Please use TLS version 1.2 or higher.", status=403) - return fetch(request) +from workers import WorkerEntrypoint, Response, fetch + +class Default(WorkerEntrypoint): + async def fetch(self, request): + tls_version = request.cf.tlsVersion + if tls_version not in ("TLSv1.2", "TLSv1.3"): + return Response("Please use TLS version 1.2 or higher.", status=403) + return fetch(request) ``` diff --git a/src/content/docs/workers/examples/bulk-origin-proxy.mdx b/src/content/docs/workers/examples/bulk-origin-proxy.mdx index 0d3f83681a14713..fde94fdb85ed59b 100644 --- a/src/content/docs/workers/examples/bulk-origin-proxy.mdx +++ b/src/content/docs/workers/examples/bulk-origin-proxy.mdx @@ -109,25 +109,27 @@ export default app; ```py +from workers import WorkerEntrypoint from js import fetch, URL -async def on_fetch(request): - # A dict with different URLs to fetch - ORIGINS = { - "starwarsapi.yourdomain.com": "swapi.dev", - "google.yourdomain.com": "www.google.com", - } +class Default(WorkerEntrypoint): + async def fetch(self, request): + # A dict with different URLs to fetch + ORIGINS = { + "starwarsapi.yourdomain.com": "swapi.dev", + "google.yourdomain.com": "www.google.com", + } - url = URL.new(request.url) + url = URL.new(request.url) - # Check if incoming hostname is a key in the ORIGINS object - if url.hostname in ORIGINS: - url.hostname = ORIGINS[url.hostname] - # If it is, proxy request to that third party origin - return fetch(url.toString(), request) + # Check if incoming hostname is a key in the ORIGINS object + if url.hostname in ORIGINS: + url.hostname = ORIGINS[url.hostname] + # If it is, proxy request to that third party origin + return fetch(url.toString(), request) - # Otherwise, process request as normal - return fetch(request) + # Otherwise, process request as normal + return fetch(request) ``` diff --git a/src/content/docs/workers/examples/bulk-redirects.mdx b/src/content/docs/workers/examples/bulk-redirects.mdx index 23932b9a139f7e9..2363b4967558612 100644 --- a/src/content/docs/workers/examples/bulk-redirects.mdx +++ b/src/content/docs/workers/examples/bulk-redirects.mdx @@ -75,27 +75,28 @@ export default { ```py -from workers import Response, fetch +from workers import WorkerEntrypoint, Response, fetch from urllib.parse import urlparse -async def on_fetch(request): - external_hostname = "examples.cloudflareworkers.com" +class Default(WorkerEntrypoint): + async def fetch(self, request): + external_hostname = "examples.cloudflareworkers.com" - redirect_map = { - "/bulk1": "https://" + external_hostname + "/redirect2", - "/bulk2": "https://" + external_hostname + "/redirect3", - "/bulk3": "https://" + external_hostname + "/redirect4", - "/bulk4": "https://google.com", - } + redirect_map = { + "/bulk1": "https://" + external_hostname + "/redirect2", + "/bulk2": "https://" + external_hostname + "/redirect3", + "/bulk3": "https://" + external_hostname + "/redirect4", + "/bulk4": "https://google.com", + } - url = urlparse(request.url) - location = redirect_map.get(url.path, None) + url = urlparse(request.url) + location = redirect_map.get(url.path, None) - if location: - return Response.redirect(location, 301) + if location: + return Response.redirect(location, 301) - # If request not in map, return the original request - return fetch(request) + # If request not in map, return the original request + return fetch(request) ``` diff --git a/src/content/docs/workers/examples/cache-api.mdx b/src/content/docs/workers/examples/cache-api.mdx index d86dc9cd892eb19..35ab3d359e3d8b2 100644 --- a/src/content/docs/workers/examples/cache-api.mdx +++ b/src/content/docs/workers/examples/cache-api.mdx @@ -106,35 +106,37 @@ export default { ```py +from workers import WorkerEntrypoint from pyodide.ffi import create_proxy from js import Response, Request, URL, caches, fetch -async def on_fetch(request, _env, ctx): - cache_url = request.url - - # Construct the cache key from the cache URL - cache_key = Request.new(cache_url, request) - 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 - response = await cache.match(cache_key) - - if response is None: - print(f"Response for request url: {request.url} not present in cache. Fetching and caching request.") - # If not in cache, get it from origin - response = await fetch(request) - # Must use Response constructor to inherit all of response's fields - response = Response.new(response.body, response) - - # Cache API respects Cache-Control headers. Setting s-max-age to 10 - # will limit the response to be in cache for 10 seconds s-maxage - # Any changes made to the response here will be reflected in the cached value - response.headers.append("Cache-Control", "s-maxage=10") - ctx.waitUntil(create_proxy(cache.put(cache_key, response.clone()))) - else: - print(f"Cache hit for: {request.url}.") - return response +class Default(WorkerEntrypoint): + async def fetch(self, request, _env, ctx): + cache_url = request.url + + # Construct the cache key from the cache URL + cache_key = Request.new(cache_url, request) + 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 + response = await cache.match(cache_key) + + if response is None: + print(f"Response for request url: {request.url} not present in cache. Fetching and caching request.") + # If not in cache, get it from origin + response = await fetch(request) + # Must use Response constructor to inherit all of response's fields + response = Response.new(response.body, response) + + # Cache API respects Cache-Control headers. Setting s-max-age to 10 + # will limit the response to be in cache for 10 seconds s-maxage + # Any changes made to the response here will be reflected in the cached value + response.headers.append("Cache-Control", "s-maxage=10") + ctx.waitUntil(create_proxy(cache.put(cache_key, response.clone()))) + else: + print(f"Cache hit for: {request.url}.") + return response ``` diff --git a/src/content/docs/workers/examples/cache-post-request.mdx b/src/content/docs/workers/examples/cache-post-request.mdx index 4167ef43c88dd98..878fa80bc2fbfc0 100644 --- a/src/content/docs/workers/examples/cache-post-request.mdx +++ b/src/content/docs/workers/examples/cache-post-request.mdx @@ -121,35 +121,37 @@ export default { ```py import hashlib +from workers import WorkerEntrypoint from pyodide.ffi import create_proxy from js import fetch, URL, Headers, Request, caches -async def on_fetch(request, _, ctx): - if 'POST' in request.method: - # Hash the request body to use it as a part of the cache key - body = await request.clone().text() - body_hash = hashlib.sha256(body.encode('UTF-8')).hexdigest() +class Default(WorkerEntrypoint): + async def fetch(self, request, _, ctx): + if 'POST' in request.method: + # Hash the request body to use it as a part of the cache key + body = await request.clone().text() + body_hash = hashlib.sha256(body.encode('UTF-8')).hexdigest() - # Store the URL in cache by prepending the body's hash - cache_url = URL.new(request.url) - cache_url.pathname = "/posts" + cache_url.pathname + body_hash + # Store the URL in cache by prepending the body's hash + cache_url = URL.new(request.url) + cache_url.pathname = "/posts" + cache_url.pathname + body_hash - # Convert to a GET to be able to cache - headers = Headers.new(dict(request.headers).items()) - cache_key = Request.new(cache_url.toString(), method='GET', headers=headers) + # Convert to a GET to be able to cache + headers = Headers.new(dict(request.headers).items()) + cache_key = Request.new(cache_url.toString(), method='GET', headers=headers) - # Find the cache key in the cache - cache = caches.default - response = await cache.match(cache_key) + # Find the cache key in the cache + cache = caches.default + response = await cache.match(cache_key) - # Otherwise, fetch response to POST request from origin - if response is None: - response = await fetch(request) - ctx.waitUntil(create_proxy(cache.put(cache_key, response.clone()))) + # Otherwise, fetch response to POST request from origin + if response is None: + response = await fetch(request) + ctx.waitUntil(create_proxy(cache.put(cache_key, response.clone()))) - return response + return response - return fetch(request) + return fetch(request) ``` diff --git a/src/content/docs/workers/examples/cache-tags.mdx b/src/content/docs/workers/examples/cache-tags.mdx index a582da8f22c1ad7..dfdb62f8f30240c 100644 --- a/src/content/docs/workers/examples/cache-tags.mdx +++ b/src/content/docs/workers/examples/cache-tags.mdx @@ -150,30 +150,32 @@ export default app; ```py +from workers import WorkerEntrypoint from pyodide.ffi import to_js as _to_js from js import Response, URL, Object, fetch def to_js(x): return _to_js(x, dict_converter=Object.fromEntries) -async def on_fetch(request): - request_url = URL.new(request.url) - params = request_url.searchParams - tags = params["tags"].split(",") if "tags" in params else [] - url = params["uri"] or None +class Default(WorkerEntrypoint): + async def fetch(self, request): + request_url = URL.new(request.url) + params = request_url.searchParams + tags = params["tags"].split(",") if "tags" in params else [] + url = params["uri"] or None - if url is None: - error = {"error": "URL cannot be empty"} - return Response.json(to_js(error), status=400) + if url is None: + error = {"error": "URL cannot be empty"} + return Response.json(to_js(error), status=400) - options = {"cf": {"cacheTags": tags}} - result = await fetch(url, to_js(options)) + options = {"cf": {"cacheTags": tags}} + result = await fetch(url, to_js(options)) - cache_status = result.headers["cf-cache-status"] - last_modified = result.headers["last-modified"] - response = {"cache": cache_status, "lastModified": last_modified} + cache_status = result.headers["cf-cache-status"] + last_modified = result.headers["last-modified"] + response = {"cache": cache_status, "lastModified": last_modified} - return Response.json(to_js(response), status=result.status) + return Response.json(to_js(response), status=result.status) ``` diff --git a/src/content/docs/workers/examples/cache-using-fetch.mdx b/src/content/docs/workers/examples/cache-using-fetch.mdx index b7540312ea7b761..667cdd1d623a258 100644 --- a/src/content/docs/workers/examples/cache-using-fetch.mdx +++ b/src/content/docs/workers/examples/cache-using-fetch.mdx @@ -124,38 +124,40 @@ export default app; ```py +from workers import WorkerEntrypoint from pyodide.ffi import to_js as _to_js from js import Response, URL, Object, fetch def to_js(x): return _to_js(x, dict_converter=Object.fromEntries) -async def on_fetch(request): - url = URL.new(request.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 - some_custom_key = f"https://{url.hostname}{url.pathname}" - - response = await fetch( - request, - cf=to_js({ - # 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": some_custom_key, - }), - ) - - # Reconstruct the Response object to make its headers mutable - response = Response.new(response.body, response) - - # Set cache control headers to cache on browser for 25 minutes - response.headers["Cache-Control"] = "max-age=1500" - - return response +class Default(WorkerEntrypoint): + async def fetch(self, request): + url = URL.new(request.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 + some_custom_key = f"https://{url.hostname}{url.pathname}" + + response = await fetch( + request, + cf=to_js({ + # 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": some_custom_key, + }), + ) + + # Reconstruct the Response object to make its headers mutable + response = Response.new(response.body, response) + + # Set cache control headers to cache on browser for 25 minutes + response.headers["Cache-Control"] = "max-age=1500" + + return response ``` diff --git a/src/content/docs/workers/examples/conditional-response.mdx b/src/content/docs/workers/examples/conditional-response.mdx index e24516cd0f8724e..c1c49938ffe8680 100644 --- a/src/content/docs/workers/examples/conditional-response.mdx +++ b/src/content/docs/workers/examples/conditional-response.mdx @@ -125,47 +125,48 @@ export default { ```py import re -from workers import Response +from workers import WorkerEntrypoint, Response, fetch from urllib.parse import urlparse -async def on_fetch(request): - blocked_hostnames = ["nope.mywebsite.com", "bye.website.com"] - url = urlparse(request.url) - - # Block on hostname - if url.hostname in blocked_hostnames: - return Response("Blocked Host", status=403) - - # On paths ending in .doc or .xml - if re.search(r'\.(doc|xml)$', url.path): - return Response("Blocked Extension", status=403) - - # On HTTP method - if "POST" in request.method: - return Response("Response for POST") - - # On User Agent - user_agent = request.headers["User-Agent"] or "" - if "bot" in user_agent: - return Response("Block User Agent containing bot", status=403) - - # On Client's IP address - client_ip = request.headers["CF-Connecting-IP"] - if client_ip == "1.2.3.4": - return Response("Block the IP 1.2.3.4", status=403) - - # On ASN - if request.cf and request.cf.asn == 64512: - return Response("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. - device = request.headers["CF-Device-Type"] - if device == "mobile": - return Response.redirect("https://mobile.example.com") - - return fetch(request) +class Default(WorkerEntrypoint): + async def fetch(self, request): + blocked_hostnames = ["nope.mywebsite.com", "bye.website.com"] + url = urlparse(request.url) + + # Block on hostname + if url.hostname in blocked_hostnames: + return Response("Blocked Host", status=403) + + # On paths ending in .doc or .xml + if re.search(r'\.(doc|xml)$', url.path): + return Response("Blocked Extension", status=403) + + # On HTTP method + if "POST" in request.method: + return Response("Response for POST") + + # On User Agent + user_agent = request.headers["User-Agent"] or "" + if "bot" in user_agent: + return Response("Block User Agent containing bot", status=403) + + # On Client's IP address + client_ip = request.headers["CF-Connecting-IP"] + if client_ip == "1.2.3.4": + return Response("Block the IP 1.2.3.4", status=403) + + # On ASN + if request.cf and request.cf.asn == 64512: + return Response("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. + device = request.headers["CF-Device-Type"] + if device == "mobile": + return Response.redirect("https://mobile.example.com") + + return fetch(request) ``` diff --git a/src/content/docs/workers/examples/cors-header-proxy.mdx b/src/content/docs/workers/examples/cors-header-proxy.mdx index 823b4d533b6623e..58e62e7db6b1cca 100644 --- a/src/content/docs/workers/examples/cors-header-proxy.mdx +++ b/src/content/docs/workers/examples/cors-header-proxy.mdx @@ -475,110 +475,112 @@ export default app; ```py +from workers import WorkerEntrypoint from pyodide.ffi import to_js as _to_js from js import Response, URL, fetch, Object, Request def to_js(x): return _to_js(x, dict_converter=Object.fromEntries) -async def on_fetch(request): - cors_headers = { - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "GET,HEAD,POST,OPTIONS", - "Access-Control-Max-Age": "86400", - } +class Default(WorkerEntrypoint): + async def fetch(self, request): + cors_headers = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET,HEAD,POST,OPTIONS", + "Access-Control-Max-Age": "86400", + } - api_url = "https://examples.cloudflareworkers.com/demos/demoapi" + api_url = "https://examples.cloudflareworkers.com/demos/demoapi" - proxy_endpoint = "/corsproxy/" + proxy_endpoint = "/corsproxy/" - def raw_html_response(html): - return Response.new(html, headers=to_js({"content-type": "text/html;charset=UTF-8"})) + def raw_html_response(html): + return Response.new(html, headers=to_js({"content-type": "text/html;charset=UTF-8"})) - demo_page = f''' - - - -

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 - - - - ''' + reqs.proxy = async () => {{ + let href = "{proxy_endpoint}?apiurl={api_url}" + return fetch(window.location.origin + href).then(r => r.json()) + }} + reqs.proxypreflight = async () => {{ + let href = "{proxy_endpoint}?apiurl={api_url}" + let response = await fetch(window.location.origin + href, {{ + method: "POST", + headers: {{ + "Content-Type": "application/json" + }}, + body: JSON.stringify({{ + msg: "Hello world!" + }}) + }}) + return response.json() + }} + (async () => {{ + for (const [reqName, req] of Object.entries(reqs)) {{ + try {{ + let data = await req() + document.getElementById(reqName).innerHTML = JSON.stringify(data) + }} catch (e) {{ + document.getElementById(reqName).innerHTML = e + }} + }} + }})() + + + + ''' + + async def handle_request(request): + url = URL.new(request.url) + api_url2 = url.searchParams["apiurl"] + + if not api_url2: + api_url2 = api_url + + request = Request.new(api_url2, request) + request.headers["Origin"] = (URL.new(api_url2)).origin + print(request.headers) + response = await fetch(request) + response = Response.new(response.body, response) + response.headers["Access-Control-Allow-Origin"] = url.origin + response.headers["Vary"] = "Origin" + return response + + async def handle_options(request): + if "Origin" in request.headers and "Access-Control-Request-Method" in request.headers and "Access-Control-Request-Headers" in request.headers: + return Response.new(None, headers=to_js({ + **cors_headers, + "Access-Control-Allow-Headers": request.headers["Access-Control-Request-Headers"] + })) + return Response.new(None, headers=to_js({"Allow": "GET, HEAD, POST, OPTIONS"})) - async def handle_request(request): url = URL.new(request.url) - api_url2 = url.searchParams["apiurl"] - - if not api_url2: - api_url2 = api_url - - request = Request.new(api_url2, request) - request.headers["Origin"] = (URL.new(api_url2)).origin - print(request.headers) - response = await fetch(request) - response = Response.new(response.body, response) - response.headers["Access-Control-Allow-Origin"] = url.origin - response.headers["Vary"] = "Origin" - return response - - async def handle_options(request): - if "Origin" in request.headers and "Access-Control-Request-Method" in request.headers and "Access-Control-Request-Headers" in request.headers: - return Response.new(None, headers=to_js({ - **cors_headers, - "Access-Control-Allow-Headers": request.headers["Access-Control-Request-Headers"] - })) - return Response.new(None, headers=to_js({"Allow": "GET, HEAD, POST, OPTIONS"})) - - url = URL.new(request.url) - - if url.pathname.startswith(proxy_endpoint): - if request.method == "OPTIONS": - return handle_options(request) - if request.method in ("GET", "HEAD", "POST"): - return handle_request(request) - return Response.new(None, status=405, statusText="Method Not Allowed") - return raw_html_response(demo_page) + + if url.pathname.startswith(proxy_endpoint): + if request.method == "OPTIONS": + return handle_options(request) + if request.method in ("GET", "HEAD", "POST"): + return handle_request(request) + return Response.new(None, status=405, statusText="Method Not Allowed") + return raw_html_response(demo_page) ``` diff --git a/src/content/docs/workers/examples/country-code-redirect.mdx b/src/content/docs/workers/examples/country-code-redirect.mdx index 15cc1d24129b970..c3f99ff288fdeb9 100644 --- a/src/content/docs/workers/examples/country-code-redirect.mdx +++ b/src/content/docs/workers/examples/country-code-redirect.mdx @@ -87,23 +87,24 @@ export default { ```py -from workers import Response, fetch +from workers import WorkerEntrypoint, Response, fetch -async def on_fetch(request): - countries = { - "US": "https://example.com/us", - "EU": "https://example.com/eu", - } +class Default(WorkerEntrypoint): + async def fetch(self, request): + countries = { + "US": "https://example.com/us", + "EU": "https://example.com/eu", + } - # 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 - country = request.cf.country + # 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 + country = request.cf.country - if country and country in countries: - url = countries[country] - return Response.redirect(url) + if country and country in countries: + url = countries[country] + return Response.redirect(url) - return fetch("https://example.com", request) + return fetch("https://example.com", request) ``` diff --git a/src/content/docs/workers/examples/data-loss-prevention.mdx b/src/content/docs/workers/examples/data-loss-prevention.mdx index 457ad2af4d7929f..5f168597e06e88a 100644 --- a/src/content/docs/workers/examples/data-loss-prevention.mdx +++ b/src/content/docs/workers/examples/data-loss-prevention.mdx @@ -173,6 +173,7 @@ export default { ```py import re +from workers import WorkerEntrypoint from datetime import datetime from js import Response, fetch, JSON, Headers @@ -187,41 +188,42 @@ async def post_data_breach(request): }) return await fetch(some_hook_server, method="POST", headers=headers, body=body) -async def on_fetch(request): - debug = True - - # Define personal data with regular expressions. - # Respond with block if credit card data, and strip - # emails and phone numbers from the response. - # Execution will be limited to MIME type "text/*". - response = await fetch(request) - - # Return origin response, if response wasn’t text - content_type = response.headers["content-type"] or "" - if "text" not in content_type: - return response - - text = await response.text() - # When debugging replace the response from the origin with an email - text = text.replace("You may use this", "me@example.com may use this") if debug else text - - sensitive_regex = [ - ("credit_card", - r'\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", r'\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b'), - ("phone", r'\b07\d{9}\b'), - ] - for (kind, regex) in sensitive_regex: - match = re.search(regex, text, flags=re.IGNORECASE) - if match: - # Alert a data breach - await post_data_breach(request) - # Respond with a block if credit card, otherwise replace sensitive text with `*`s - card_resp = Response.new(kind + " found\nForbidden\n", status=403,statusText="Forbidden") - sensitive_resp = Response.new(re.sub(regex, "*"*10, text, flags=re.IGNORECASE), response) - return card_resp if kind == "credit_card" else sensitive_resp - - return Response.new(text, response) +class Default(WorkerEntrypoint): + async def fetch(self, request): + debug = True + + # Define personal data with regular expressions. + # Respond with block if credit card data, and strip + # emails and phone numbers from the response. + # Execution will be limited to MIME type "text/*". + response = await fetch(request) + + # Return origin response, if response wasn’t text + content_type = response.headers["content-type"] or "" + if "text" not in content_type: + return response + + text = await response.text() + # When debugging replace the response from the origin with an email + text = text.replace("You may use this", "me@example.com may use this") if debug else text + + sensitive_regex = [ + ("credit_card", + r'\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", r'\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b'), + ("phone", r'\b07\d{9}\b'), + ] + for (kind, regex) in sensitive_regex: + match = re.search(regex, text, flags=re.IGNORECASE) + if match: + # Alert a data breach + await post_data_breach(request) + # Respond with a block if credit card, otherwise replace sensitive text with `*`s + card_resp = Response.new(kind + " found\nForbidden\n", status=403,statusText="Forbidden") + sensitive_resp = Response.new(re.sub(regex, "*"*10, text, flags=re.IGNORECASE), response) + return card_resp if kind == "credit_card" else sensitive_resp + + return Response.new(text, response) ``` diff --git a/src/content/docs/workers/examples/debugging-logs.mdx b/src/content/docs/workers/examples/debugging-logs.mdx index 2e10a1c28101bc0..c7fbcd774b6ec1c 100644 --- a/src/content/docs/workers/examples/debugging-logs.mdx +++ b/src/content/docs/workers/examples/debugging-logs.mdx @@ -115,36 +115,38 @@ export default { ```py +from workers import WorkerEntrypoint import json import traceback from pyodide.ffi import create_once_callable from js import Response, fetch, Headers -async def on_fetch(request, _env, ctx): - # Service configured to receive logs - log_url = "https://log-service.example.com/" - - async def post_log(data): - return await fetch(log_url, method="POST", body=data) - - response = await fetch(request) - - try: - if not response.ok and not response.redirected: - body = await response.text() - # Simulating an error. Ensure the string is small enough to be a header - raise Exception(f'Bad response at origin. Status:{response.status} Body:{body.strip()[:10]}') - except Exception as e: - # Without ctx.waitUntil(), your fetch() to Cloudflare's - # logging service may or may not complete - ctx.waitUntil(create_once_callable(post_log(e))) - stack = json.dumps(traceback.format_exc()) or e - # Copy the response and add to header - response = Response.new(stack, response) - response.headers["X-Debug-stack"] = stack - response.headers["X-Debug-err"] = e - - return response +class Default(WorkerEntrypoint): + async def fetch(self, request, _env, ctx): + # Service configured to receive logs + log_url = "https://log-service.example.com/" + + async def post_log(data): + return await fetch(log_url, method="POST", body=data) + + response = await fetch(request) + + try: + if not response.ok and not response.redirected: + body = await response.text() + # Simulating an error. Ensure the string is small enough to be a header + raise Exception(f'Bad response at origin. Status:{response.status} Body:{body.strip()[:10]}') + except Exception as e: + # Without ctx.waitUntil(), your fetch() to Cloudflare's + # logging service may or may not complete + ctx.waitUntil(create_once_callable(post_log(e))) + stack = json.dumps(traceback.format_exc()) or e + # Copy the response and add to header + response = Response.new(stack, response) + response.headers["X-Debug-stack"] = stack + response.headers["X-Debug-err"] = e + + return response ``` diff --git a/src/content/docs/workers/examples/extract-cookie-value.mdx b/src/content/docs/workers/examples/extract-cookie-value.mdx index 139ee49613b887c..7a22024badcb189 100644 --- a/src/content/docs/workers/examples/extract-cookie-value.mdx +++ b/src/content/docs/workers/examples/extract-cookie-value.mdx @@ -63,19 +63,20 @@ export default { ```py from http.cookies import SimpleCookie -from workers import Response +from workers import WorkerEntrypoint, Response -async def on_fetch(request): - # Name of the cookie - cookie_name = "__uid" +class Default(WorkerEntrypoint): + async def fetch(self, request): + # Name of the cookie + cookie_name = "__uid" - cookies = SimpleCookie(request.headers["Cookie"] or "") + cookies = SimpleCookie(request.headers["Cookie"] or "") - if cookie_name in cookies: - # Respond with cookie value - return Response(cookies[cookie_name].value) + if cookie_name in cookies: + # Respond with cookie value + return Response(cookies[cookie_name].value) - return Response("No cookie with name: " + cookie_name) + return Response("No cookie with name: " + cookie_name) ``` diff --git a/src/content/docs/workers/examples/fetch-html.mdx b/src/content/docs/workers/examples/fetch-html.mdx index fa5d2197ef84462..b0eccdbad7aecea 100644 --- a/src/content/docs/workers/examples/fetch-html.mdx +++ b/src/content/docs/workers/examples/fetch-html.mdx @@ -47,12 +47,14 @@ export default { ```py +from workers import WorkerEntrypoint from js import fetch -async def on_fetch(request): - # Replace `remote` with the host you wish to send requests to - remote = "https://example.com" - return await fetch(remote, request) +class Default(WorkerEntrypoint): + async def fetch(self, request): + # Replace `remote` with the host you wish to send requests to + remote = "https://example.com" + return await fetch(remote, request) ``` diff --git a/src/content/docs/workers/examples/fetch-json.mdx b/src/content/docs/workers/examples/fetch-json.mdx index 612791bc97af4a7..6aee14f6c622763 100644 --- a/src/content/docs/workers/examples/fetch-json.mdx +++ b/src/content/docs/workers/examples/fetch-json.mdx @@ -79,27 +79,27 @@ export default { ```py -from workers import Response, fetch +from workers import WorkerEntrypoint, Response, fetch import json -async def on_fetch(request): - url = "https://jsonplaceholder.typicode.com/todos/1" +class Default(WorkerEntrypoint): + async def fetch(self, request): + url = "https://jsonplaceholder.typicode.com/todos/1" - # gather_response returns both content-type & response body as a string - async def gather_response(response): - headers = response.headers - content_type = headers["content-type"] or "" + # gather_response returns both content-type & response body as a string + async def gather_response(response): + headers = response.headers + content_type = headers["content-type"] or "" - if "application/json" in content_type: - return (content_type, json.dumps(await response.json())) - return (content_type, await response.text()) + if "application/json" in content_type: + return (content_type, json.dumps(await response.json())) + return (content_type, await response.text()) - response = await fetch(url) - content_type, result = await gather_response(response) + response = await fetch(url) + content_type, result = await gather_response(response) - - headers = {"content-type": content_type} - return Response(result, headers=headers) + headers = {"content-type": content_type} + return Response(result, headers=headers) ``` diff --git a/src/content/docs/workers/examples/geolocation-app-weather.mdx b/src/content/docs/workers/examples/geolocation-app-weather.mdx index c1d4664861f8a2a..0fb3336eba6b149 100644 --- a/src/content/docs/workers/examples/geolocation-app-weather.mdx +++ b/src/content/docs/workers/examples/geolocation-app-weather.mdx @@ -211,44 +211,45 @@ export default app; ```py -from workers import Response, fetch - -async def on_fetch(request): - endpoint = "https://api.waqi.info/feed/geo:" - token = "" # Use a token from https://aqicn.org/api/ - html_style = "body{padding:6em; font-family: sans-serif;} h1{color:#f6821f}" - html_content = "

Weather 🌦

" - - latitude = request.cf.latitude - longitude = request.cf.longitude - - endpoint += f"{latitude};{longitude}/?token={token}" - response = await fetch(endpoint) - content = await response.json() - - html_content += "

This is a demo using Workers geolocation data.

" - html_content += f"You are located at: {latitude},{longitude}.

" - html_content += f"

Based off sensor data from {content['data']['city']['name']}:

" - html_content += f"

The AQI level is: {content['data']['aqi']}.

" - html_content += f"

The N02 level is: {content['data']['iaqi']['no2']['v']}.

" - html_content += f"

The O3 level is: {content['data']['iaqi']['o3']['v']}.

" - html_content += f"

The temperature is: {content['data']['iaqi']['t']['v']}°C.

" - - html = f""" - - - Geolocation: Weather - - - -
- {html_content} -
- - """ - - headers = {"content-type": "text/html;charset=UTF-8"} - return Response(html, headers=headers) +from workers import WorkerEntrypoint, Response, fetch + +class Default(WorkerEntrypoint): + async def fetch(self, request): + endpoint = "https://api.waqi.info/feed/geo:" + token = "" # Use a token from https://aqicn.org/api/ + html_style = "body{padding:6em; font-family: sans-serif;} h1{color:#f6821f}" + html_content = "

Weather 🌦

" + + latitude = request.cf.latitude + longitude = request.cf.longitude + + endpoint += f"{latitude};{longitude}/?token={token}" + response = await fetch(endpoint) + content = await response.json() + + html_content += "

This is a demo using Workers geolocation data.

" + html_content += f"You are located at: {latitude},{longitude}.

" + html_content += f"

Based off sensor data from {content['data']['city']['name']}:

" + html_content += f"

The AQI level is: {content['data']['aqi']}.

" + html_content += f"

The N02 level is: {content['data']['iaqi']['no2']['v']}.

" + html_content += f"

The O3 level is: {content['data']['iaqi']['o3']['v']}.

" + html_content += f"

The temperature is: {content['data']['iaqi']['t']['v']}°C.

" + + html = f""" + + + Geolocation: Weather + + + +
+ {html_content} +
+ + """ + + headers = {"content-type": "text/html;charset=UTF-8"} + return Response(html, headers=headers) ```
diff --git a/src/content/docs/workers/examples/geolocation-hello-world.mdx b/src/content/docs/workers/examples/geolocation-hello-world.mdx index 4f6f79640e45faf..73230f5e02e7ffb 100644 --- a/src/content/docs/workers/examples/geolocation-hello-world.mdx +++ b/src/content/docs/workers/examples/geolocation-hello-world.mdx @@ -108,38 +108,39 @@ export default { ```py -from workers import Response - -async def on_fetch(request): - html_content = "" - html_style = "body{padding:6em font-family: sans-serif;} h1{color:#f6821f;}" - - html_content += "

Colo: " + request.cf.colo + "

" - html_content += "

Country: " + request.cf.country + "

" - html_content += "

City: " + request.cf.city + "

" - html_content += "

Continent: " + request.cf.continent + "

" - html_content += "

Latitude: " + request.cf.latitude + "

" - html_content += "

Longitude: " + request.cf.longitude + "

" - html_content += "

PostalCode: " + request.cf.postalCode + "

" - html_content += "

Region: " + request.cf.region + "

" - html_content += "

RegionCode: " + request.cf.regionCode + "

" - html_content += "

Timezone: " + request.cf.timezone + "

" - - html = f""" - - - Geolocation: Hello World - - - -

Geolocation: Hello World!

-

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

- {html_content} - - """ - - headers = {"content-type": "text/html;charset=UTF-8"} - return Response(html, headers=headers) +from workers import WorkerEntrypoint, Response + +class Default(WorkerEntrypoint): + async def fetch(self, request): + html_content = "" + html_style = "body{padding:6em font-family: sans-serif;} h1{color:#f6821f;}" + + html_content += "

Colo: " + request.cf.colo + "

" + html_content += "

Country: " + request.cf.country + "

" + html_content += "

City: " + request.cf.city + "

" + html_content += "

Continent: " + request.cf.continent + "

" + html_content += "

Latitude: " + request.cf.latitude + "

" + html_content += "

Longitude: " + request.cf.longitude + "

" + html_content += "

PostalCode: " + request.cf.postalCode + "

" + html_content += "

Region: " + request.cf.region + "

" + html_content += "

RegionCode: " + request.cf.regionCode + "

" + html_content += "

Timezone: " + request.cf.timezone + "

" + + html = f""" + + + Geolocation: Hello World + + + +

Geolocation: Hello World!

+

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

+ {html_content} + + """ + + headers = {"content-type": "text/html;charset=UTF-8"} + return Response(html, headers=headers) ```
diff --git a/src/content/docs/workers/examples/hot-link-protection.mdx b/src/content/docs/workers/examples/hot-link-protection.mdx index 9f9a78d01528070..8a10b25a95775ab 100644 --- a/src/content/docs/workers/examples/hot-link-protection.mdx +++ b/src/content/docs/workers/examples/hot-link-protection.mdx @@ -87,28 +87,29 @@ export default { ```py -from workers import Response, fetch +from workers import WorkerEntrypoint, Response, fetch from urllib.parse import urlparse -async def on_fetch(request): - homepage_url = "https://tutorial.cloudflareworkers.com/" - protected_type = "image/" +class Default(WorkerEntrypoint): + async def fetch(self, request): + homepage_url = "https://tutorial.cloudflareworkers.com/" + protected_type = "image/" - # Fetch the original request - response = await fetch(request) + # Fetch the original request + response = await fetch(request) - # If it's an image, engage hotlink protection based on the referer header - referer = request.headers["Referer"] - content_type = response.headers["Content-Type"] or "" + # If it's an image, engage hotlink protection based on the referer header + referer = request.headers["Referer"] + content_type = response.headers["Content-Type"] or "" - if referer and content_type.startswith(protected_type): - # If the hostnames don't match, it's a hotlink - if urlparse(referer).hostname != urlparse(request.url).hostname: - # Redirect the user to your website - return Response.redirect(homepage_url, 302) + if referer and content_type.startswith(protected_type): + # If the hostnames don't match, it's a hotlink + if urlparse(referer).hostname != urlparse(request.url).hostname: + # Redirect the user to your website + return Response.redirect(homepage_url, 302) - # Everything is fine, return the response normally - return response + # Everything is fine, return the response normally + return response ``` diff --git a/src/content/docs/workers/examples/images-workers.mdx b/src/content/docs/workers/examples/images-workers.mdx index b37bde7082ccb01..e72f3cfb6497975 100644 --- a/src/content/docs/workers/examples/images-workers.mdx +++ b/src/content/docs/workers/examples/images-workers.mdx @@ -96,16 +96,18 @@ export default app; ```py +from workers import WorkerEntrypoint from js import URL, fetch -async def on_fetch(request): - # You can find this in the dashboard, it should look something like this: ZWd9g1K7eljCn_KDTu_MWA - account_hash = "" - url = URL.new(request.url) +class Default(WorkerEntrypoint): + async def fetch(self, request): + # You can find this in the dashboard, it should look something like this: ZWd9g1K7eljCn_KDTu_MWA + account_hash = "" + url = URL.new(request.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(f'https://imagedelivery.net/{account_hash}{url.pathname}') + # 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(f'https://imagedelivery.net/{account_hash}{url.pathname}') ``` diff --git a/src/content/docs/workers/examples/logging-headers.mdx b/src/content/docs/workers/examples/logging-headers.mdx index 2c7cb0c03579c10..9d8748409713179 100644 --- a/src/content/docs/workers/examples/logging-headers.mdx +++ b/src/content/docs/workers/examples/logging-headers.mdx @@ -50,11 +50,12 @@ export default { ```py -from workers import Response +from workers import WorkerEntrypoint, Response -async def on_fetch(request): - print(dict(request.headers)) - return Response('Hello world') +class Default(WorkerEntrypoint): + async def fetch(self, request): + print(dict(request.headers)) + return Response('Hello world') ``` diff --git a/src/content/docs/workers/examples/modify-request-property.mdx b/src/content/docs/workers/examples/modify-request-property.mdx index a1abfdeb89aa312..ce6da04b8187b79 100644 --- a/src/content/docs/workers/examples/modify-request-property.mdx +++ b/src/content/docs/workers/examples/modify-request-property.mdx @@ -146,47 +146,49 @@ export default { ```py import json +from workers import WorkerEntrypoint from pyodide.ffi import to_js as _to_js from js import Object, URL, Request, fetch, Response def to_js(obj): return _to_js(obj, dict_converter=Object.fromEntries) -async def on_fetch(request): - some_host = "example.com" - some_url = "https://foo.example.com/api.js" - - # The best practice is to only assign new_request_init properties - # on the request object using either a method or the constructor - new_request_init = { - "method": "POST", # Change method - "body": json.dumps({ "bar": "foo" }), # Change body - "redirect": "follow", # Change the redirect mode - # Change headers, note this method will erase existing headers - "headers": { - "Content-Type": "application/json", - }, - # Change a Cloudflare feature on the outbound response - "cf": { "apps": False }, - } - - # Change just the host - url = URL.new(some_url) - url.hostname = some_host - - # Best practice is to always use the original request to construct the new request - # to clone all the attributes. Applying the URL also requires a constructor - # since once a Request has been constructed, its URL is immutable. - org_request = Request.new(request, new_request_init) - new_request = Request.new(url.toString(),org_request) - - new_request.headers["X-Example"] = "bar" - new_request.headers["Content-Type"] = "application/json" - - try: - return await fetch(new_request) - except Exception as e: - return Response.new({"error": str(e)}, status=500) +class Default(WorkerEntrypoint): + async def fetch(self, request): + some_host = "example.com" + some_url = "https://foo.example.com/api.js" + + # The best practice is to only assign new_request_init properties + # on the request object using either a method or the constructor + new_request_init = { + "method": "POST", # Change method + "body": json.dumps({ "bar": "foo" }), # Change body + "redirect": "follow", # Change the redirect mode + # Change headers, note this method will erase existing headers + "headers": { + "Content-Type": "application/json", + }, + # Change a Cloudflare feature on the outbound response + "cf": { "apps": False }, + } + + # Change just the host + url = URL.new(some_url) + url.hostname = some_host + + # Best practice is to always use the original request to construct the new request + # to clone all the attributes. Applying the URL also requires a constructor + # since once a Request has been constructed, its URL is immutable. + org_request = Request.new(request, new_request_init) + new_request = Request.new(url.toString(),org_request) + + new_request.headers["X-Example"] = "bar" + new_request.headers["Content-Type"] = "application/json" + + try: + return await fetch(new_request) + except Exception as e: + return Response.new({"error": str(e)}, status=500) ``` diff --git a/src/content/docs/workers/examples/modify-response.mdx b/src/content/docs/workers/examples/modify-response.mdx index 774fefe68e09ac9..e2fe68f5b505e41 100644 --- a/src/content/docs/workers/examples/modify-response.mdx +++ b/src/content/docs/workers/examples/modify-response.mdx @@ -127,35 +127,36 @@ export default { ```py -from workers import Response, fetch +from workers import WorkerEntrypoint, Response, fetch import json -async def on_fetch(request): - header_name_src = "foo" # Header to get the new value from - header_name_dst = "Last-Modified" # Header to set based off of value in src +class Default(WorkerEntrypoint): + async def fetch(self, request): + header_name_src = "foo" # Header to get the new value from + header_name_dst = "Last-Modified" # Header to set based off of value in src - # Response properties are immutable. To change them, construct a new response - original_response = await fetch(request) + # Response properties are immutable. To change them, construct a new response + original_response = await fetch(request) - # Change status and statusText, but preserve body and headers - response = Response(original_response.body, status=500, status_text="some message", headers=original_response.headers) + # Change status and statusText, but preserve body and headers + response = Response(original_response.body, status=500, status_text="some message", headers=original_response.headers) - # Change response body by adding the foo prop - new_body = await original_response.json() - new_body["foo"] = "bar" - response.replace_body(json.dumps(new_body)) + # Change response body by adding the foo prop + new_body = await original_response.json() + new_body["foo"] = "bar" + response.replace_body(json.dumps(new_body)) - # Add a new header - response.headers["foo"] = "bar" + # Add a new header + response.headers["foo"] = "bar" - # Set destination header to the value of the source header - src = response.headers[header_name_src] + # Set destination header to the value of the source header + src = response.headers[header_name_src] - if src is not None: - response.headers[header_name_dst] = src - print(f'Response header {header_name_dst} was set to {response.headers[header_name_dst]}') + if src is not None: + response.headers[header_name_dst] = src + print(f'Response header {header_name_dst} was set to {response.headers[header_name_dst]}') - return response + return response ``` diff --git a/src/content/docs/workers/examples/post-json.mdx b/src/content/docs/workers/examples/post-json.mdx index 9d6042018f1bf90..8d3a7edb399e73d 100644 --- a/src/content/docs/workers/examples/post-json.mdx +++ b/src/content/docs/workers/examples/post-json.mdx @@ -129,6 +129,7 @@ export default { ```py import json +from workers import WorkerEntrypoint from pyodide.ffi import to_js as _to_js from js import Object, fetch, Response, Headers @@ -144,28 +145,29 @@ async def gather_response(response): return (content_type, json.dumps(dict(await response.json()))) return (content_type, await response.text()) -async def on_fetch(_request): - url = "https://jsonplaceholder.typicode.com/todos/1" +class Default(WorkerEntrypoint): + async def fetch(self, _request): + url = "https://jsonplaceholder.typicode.com/todos/1" - body = { - "results": ["default data to send"], - "errors": None, - "msg": "I sent this to the fetch", - } + body = { + "results": ["default data to send"], + "errors": None, + "msg": "I sent this to the fetch", + } - options = { - "body": json.dumps(body), - "method": "POST", - "headers": { - "content-type": "application/json;charset=UTF-8", - }, - } + options = { + "body": json.dumps(body), + "method": "POST", + "headers": { + "content-type": "application/json;charset=UTF-8", + }, + } - response = await fetch(url, to_js(options)) - content_type, result = await gather_response(response) + response = await fetch(url, to_js(options)) + content_type, result = await gather_response(response) - headers = Headers.new({"content-type": content_type}.items()) - return Response.new(result, headers=headers) + headers = Headers.new({"content-type": content_type}.items()) + return Response.new(result, headers=headers) ``` 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 6f2733606472161..ee97bc3eda2551a 100644 --- a/src/content/docs/workers/examples/protect-against-timing-attacks.mdx +++ b/src/content/docs/workers/examples/protect-against-timing-attacks.mdx @@ -73,30 +73,31 @@ export default { ```py -from workers import Response +from workers import WorkerEntrypoint, Response from js import TextEncoder, crypto -async def on_fetch(request, env): - auth_token = request.headers["Authorization"] or "" - secret = env.MY_SECRET_VALUE +class Default(WorkerEntrypoint): + async def fetch(self, request, env): + auth_token = request.headers["Authorization"] or "" + secret = env.MY_SECRET_VALUE - if secret is None: - return Response("Missing secret binding", status=500) + if secret is None: + return Response("Missing secret binding", status=500) - if len(auth_token) != len(secret): - return Response("Unauthorized", status=401) + if len(auth_token) != len(secret): + return Response("Unauthorized", status=401) - encoder = TextEncoder.new() - a = encoder.encode(auth_token) - b = encoder.encode(secret) + encoder = TextEncoder.new() + a = encoder.encode(auth_token) + b = encoder.encode(secret) - if a.byteLength != b.byteLength: - return Response("Unauthorized", status=401) + if a.byteLength != b.byteLength: + return Response("Unauthorized", status=401) - if not crypto.subtle.timingSafeEqual(a, b): - return Response("Unauthorized", status=401) + if not crypto.subtle.timingSafeEqual(a, b): + return Response("Unauthorized", status=401) - return Response("Welcome!") + return Response("Welcome!") ``` diff --git a/src/content/docs/workers/examples/read-post.mdx b/src/content/docs/workers/examples/read-post.mdx index f29db417055bedd..505c7ac32b68563 100644 --- a/src/content/docs/workers/examples/read-post.mdx +++ b/src/content/docs/workers/examples/read-post.mdx @@ -147,7 +147,7 @@ export default { ```py - +from workers import WorkerEntrypoint from js import Object, Response, Headers, JSON async def read_request_body(request): @@ -162,20 +162,21 @@ async def read_request_body(request): return JSON.stringify(data) return await request.text() -async def on_fetch(request): - def raw_html_response(html): - headers = Headers.new({"content-type": "text/html;charset=UTF-8"}.items()) - return Response.new(html, headers=headers) +class Default(WorkerEntrypoint): + async def fetch(self, request): + def raw_html_response(html): + headers = Headers.new({"content-type": "text/html;charset=UTF-8"}.items()) + return Response.new(html, headers=headers) - if "form" in request.url: - return raw_html_response("") + if "form" in request.url: + return raw_html_response("") - if "POST" in request.method: - req_body = await read_request_body(request) - ret_body = f"The request body sent in was {req_body}" - return Response.new(ret_body) + if "POST" in request.method: + req_body = await read_request_body(request) + ret_body = f"The request body sent in was {req_body}" + return Response.new(ret_body) - return Response.new("The request was not POST") + return Response.new("The request was not POST") ``` diff --git a/src/content/docs/workers/examples/redirect.mdx b/src/content/docs/workers/examples/redirect.mdx index 459edde3ef0833d..b4b6127a1013c4a 100644 --- a/src/content/docs/workers/examples/redirect.mdx +++ b/src/content/docs/workers/examples/redirect.mdx @@ -48,12 +48,13 @@ export default { ```py -from workers import Response +from workers import WorkerEntrypoint, Response -def on_fetch(request): - destinationURL = "https://example.com" - statusCode = 301 - return Response.redirect(destinationURL, statusCode) +class Default(WorkerEntrypoint): + def fetch(self, request): + destinationURL = "https://example.com" + statusCode = 301 + return Response.redirect(destinationURL, statusCode) ``` @@ -131,19 +132,20 @@ export default { ```py -from workers import Response +from workers import WorkerEntrypoint, Response from urllib.parse import urlparse -async def on_fetch(request): - base = "https://example.com" - statusCode = 301 +class Default(WorkerEntrypoint): + async def fetch(self, request): + base = "https://example.com" + statusCode = 301 - url = urlparse(request.url) + url = urlparse(request.url) - destinationURL = f'{base}{url.path}{url.query}' - print(destinationURL) + destinationURL = f'{base}{url.path}{url.query}' + print(destinationURL) - return Response.redirect(destinationURL, statusCode) + return Response.redirect(destinationURL, statusCode) ``` 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 7ae2faa863d7d23..6a9de6fa669907b 100644 --- a/src/content/docs/workers/examples/respond-with-another-site.mdx +++ b/src/content/docs/workers/examples/respond-with-another-site.mdx @@ -53,19 +53,20 @@ export default { ```py -from workers import Response, fetch +from workers import WorkerEntrypoint, Response, fetch -def on_fetch(request): - def method_not_allowed(request): - msg = f'Method {request.method} not allowed.' - headers = {"Allow": "GET"} - return Response(msg, headers=headers, status=405) +class Default(WorkerEntrypoint): + def fetch(self, request): + def method_not_allowed(request): + msg = f'Method {request.method} not allowed.' + headers = {"Allow": "GET"} + return Response(msg, headers=headers, status=405) - # Only GET requests work with this proxy. - if request.method != "GET": - return method_not_allowed(request) + # Only GET requests work with this proxy. + if request.method != "GET": + return method_not_allowed(request) - return fetch("https://example.com") + 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 3fe0640632f2775..dbd3ac4aaa2c0ac 100644 --- a/src/content/docs/workers/examples/return-html.mdx +++ b/src/content/docs/workers/examples/return-html.mdx @@ -53,17 +53,18 @@ export default { ```py -from workers import Response - -def on_fetch(request): - html = """ - -

Hello World

-

This markup was generated by a Cloudflare Worker.

- """ - - headers = {"content-type": "text/html;charset=UTF-8"} - return Response(html, headers=headers) +from workers import WorkerEntrypoint, Response + +class Default(WorkerEntrypoint): + async def fetch(self, request): + html = """ + +

Hello World

+

This markup was generated by a Cloudflare Worker.

+ """ + + headers = {"content-type": "text/html;charset=UTF-8"} + return Response(html, headers=headers) ```
diff --git a/src/content/docs/workers/examples/return-json.mdx b/src/content/docs/workers/examples/return-json.mdx index 906024f982b0acf..42f37fcf0b7783b 100644 --- a/src/content/docs/workers/examples/return-json.mdx +++ b/src/content/docs/workers/examples/return-json.mdx @@ -49,13 +49,14 @@ export default { ```py -from workers import Response +from workers import WorkerEntrypoint, Response import json -def on_fetch(request): - data = json.dumps({"hello": "world"}) - headers = {"content-type": "application/json"} - return Response(data, headers=headers) +class Default(WorkerEntrypoint): + def fetch(self, request): + data = json.dumps({"hello": "world"}) + headers = {"content-type": "application/json"} + return Response(data, headers=headers) ``` diff --git a/src/content/docs/workers/examples/rewrite-links.mdx b/src/content/docs/workers/examples/rewrite-links.mdx index cdcf89ab7089cfb..48a6b1273aad2fb 100644 --- a/src/content/docs/workers/examples/rewrite-links.mdx +++ b/src/content/docs/workers/examples/rewrite-links.mdx @@ -107,32 +107,34 @@ export default { ```py +from workers import WorkerEntrypoint from pyodide.ffi import create_proxy from js import HTMLRewriter, fetch -async def on_fetch(request): - old_url = "developer.mozilla.org" - new_url = "mynewdomain.com" - - class AttributeRewriter: - def __init__(self, attr_name): - self.attr_name = attr_name - def element(self, element): - attr = element.getAttribute(self.attr_name) - if attr: - element.setAttribute(self.attr_name, attr.replace(old_url, new_url)) - - href = create_proxy(AttributeRewriter("href")) - src = create_proxy(AttributeRewriter("src")) - rewriter = HTMLRewriter.new().on("a", href).on("img", src) - res = await fetch(request) - content_type = res.headers["Content-Type"] - - # If the response is HTML, it can be transformed with - # HTMLRewriter -- otherwise, it should pass through - if content_type.startswith("text/html"): - return rewriter.transform(res) - return res +class Default(WorkerEntrypoint): + async def fetch(self, request): + old_url = "developer.mozilla.org" + new_url = "mynewdomain.com" + + class AttributeRewriter: + def __init__(self, attr_name): + self.attr_name = attr_name + def element(self, element): + attr = element.getAttribute(self.attr_name) + if attr: + element.setAttribute(self.attr_name, attr.replace(old_url, new_url)) + + href = create_proxy(AttributeRewriter("href")) + src = create_proxy(AttributeRewriter("src")) + rewriter = HTMLRewriter.new().on("a", href).on("img", src) + res = await fetch(request) + content_type = res.headers["Content-Type"] + + # If the response is HTML, it can be transformed with + # HTMLRewriter -- otherwise, it should pass through + if content_type.startswith("text/html"): + return rewriter.transform(res) + return res ``` diff --git a/src/content/docs/workers/examples/security-headers.mdx b/src/content/docs/workers/examples/security-headers.mdx index 77e52a499959a00..ac5417285cda442 100644 --- a/src/content/docs/workers/examples/security-headers.mdx +++ b/src/content/docs/workers/examples/security-headers.mdx @@ -206,54 +206,55 @@ export default { ```py -from workers import Response, fetch - -async def on_fetch(request): - 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/Reference/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/Reference/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/Reference/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/Reference/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", - } - blocked_headers = ["Public-Key-Pins", "X-Powered-By" ,"X-AspNet-Version"] - - res = await fetch(request) - new_headers = res.headers - - # This sets the headers for HTML responses - if "text/html" in new_headers["Content-Type"]: +from workers import WorkerEntrypoint, Response, fetch + +class Default(WorkerEntrypoint): + async def fetch(self, request): + 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/Reference/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/Reference/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/Reference/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/Reference/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", + } + blocked_headers = ["Public-Key-Pins", "X-Powered-By" ,"X-AspNet-Version"] + + res = await fetch(request) + new_headers = res.headers + + # This sets the headers for HTML responses + if "text/html" in new_headers["Content-Type"]: + return Response(res.body, status=res.status, statusText=res.statusText, headers=new_headers) + + for name in default_security_headers: + new_headers[name] = default_security_headers[name] + + for name in blocked_headers: + del new_headers["name"] + + tls = request.cf.tlsVersion + + if not tls in ("TLSv1.2", "TLSv1.3"): + return Response("You need to use TLS version 1.2 or higher.", status=400) return Response(res.body, status=res.status, statusText=res.statusText, headers=new_headers) - - for name in default_security_headers: - new_headers[name] = default_security_headers[name] - - for name in blocked_headers: - del new_headers["name"] - - tls = request.cf.tlsVersion - - if not tls in ("TLSv1.2", "TLSv1.3"): - return Response("You need to use TLS version 1.2 or higher.", status=400) - return Response(res.body, status=res.status, statusText=res.statusText, headers=new_headers) ``` diff --git a/src/content/docs/workers/examples/signing-requests.mdx b/src/content/docs/workers/examples/signing-requests.mdx index 1472e611879c0a1..67745e43ad8326f 100644 --- a/src/content/docs/workers/examples/signing-requests.mdx +++ b/src/content/docs/workers/examples/signing-requests.mdx @@ -374,6 +374,7 @@ export default app; ```py +from workers import WorkerEntrypoint from pyodide.ffi import to_js as _to_js from js import Response, URL, TextEncoder, Buffer, fetch, Object, crypto @@ -385,75 +386,76 @@ 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") +class Default(WorkerEntrypoint): + async def fetch(self, 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"] - ) + # 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) + url = URL.new(request.url) - if url.pathname.startswith("/generate/"): - url.pathname = url.pathname.replace("/generate/", "/", 1) + if url.pathname.startswith("/generate/"): + url.pathname = url.pathname.replace("/generate/", "/", 1) - timestamp = int(Date.now() / 1000) + timestamp = int(Date.now() / 1000) - # Data to authenticate - data_to_authenticate = f"{url.pathname}{timestamp}" + # 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) - ) + # 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") + # Convert to base64 + base64_mac = Buffer.from(mac).toString("base64") - # Set the verification parameter - url.searchParams.set("verify", f"{timestamp}-{base64_mac}") + # 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) + 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("-") + verify_param = url.searchParams.get("verify") + timestamp, hmac = verify_param.split("-") - asserted_timestamp = int(timestamp) + asserted_timestamp = int(timestamp) - data_to_authenticate = f"{url.pathname}{asserted_timestamp}" + data_to_authenticate = f"{url.pathname}{asserted_timestamp}" - received_mac = Buffer.from(hmac, "base64") + received_mac = Buffer.from(hmac, "base64") - # Verify the signature - verified = await crypto.subtle.verify( - "HMAC", - key, - received_mac, - encoder.encode(data_to_authenticate) - ) + # 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) + 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) + # 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) + # 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/turnstile-html-rewriter.mdx b/src/content/docs/workers/examples/turnstile-html-rewriter.mdx index 846d443c2d79b47..bba6083fb608e4f 100644 --- a/src/content/docs/workers/examples/turnstile-html-rewriter.mdx +++ b/src/content/docs/workers/examples/turnstile-html-rewriter.mdx @@ -212,34 +212,36 @@ export default app; ```py +from workers import WorkerEntrypoint from pyodide.ffi import create_proxy from js import HTMLRewriter, fetch -async def on_fetch(request, env): - site_key = env.SITE_KEY - attr_name = env.TURNSTILE_ATTR_NAME - res = await fetch(request) - - class Append: - def element(self, element): - s = '' - element.append(s, {"html": True}) - - class AppendOnID: - def __init__(self, name): - self.name = name - def element(self, element): - # You are using the `getAttribute` method here to retrieve the `id` or `class` of an element - if element.getAttribute("id") == self.name: - div = f'
' - element.append(div, { "html": True }) - - # Instantiate the API to run on specific elements, for example, `head`, `div` - head = create_proxy(Append()) - div = create_proxy(AppendOnID(attr_name)) - new_res = HTMLRewriter.new().on("head", head).on("div", div).transform(res) - - return new_res +class Default(WorkerEntrypoint): + async def fetch(self, request, env): + site_key = env.SITE_KEY + attr_name = env.TURNSTILE_ATTR_NAME + res = await fetch(request) + + class Append: + def element(self, element): + s = '' + element.append(s, {"html": True}) + + class AppendOnID: + def __init__(self, name): + self.name = name + def element(self, element): + # You are using the `getAttribute` method here to retrieve the `id` or `class` of an element + if element.getAttribute("id") == self.name: + div = f'
' + element.append(div, { "html": True }) + + # Instantiate the API to run on specific elements, for example, `head`, `div` + head = create_proxy(Append()) + div = create_proxy(AppendOnID(attr_name)) + new_res = HTMLRewriter.new().on("head", head).on("div", div).transform(res) + + return new_res ```
diff --git a/src/content/docs/workers/languages/python/examples.mdx b/src/content/docs/workers/languages/python/examples.mdx index e9f81c2247a3047..75edbbdd8494b90 100644 --- a/src/content/docs/workers/languages/python/examples.mdx +++ b/src/content/docs/workers/languages/python/examples.mdx @@ -14,34 +14,36 @@ In addition to those examples, consider the following ones that illustrate Pytho ## Parse an incoming request URL ```python -from workers import Response +from workers import WorkerEntrypoint, Response from urllib.parse import urlparse, parse_qs -async def on_fetch(request, env): - # Parse the incoming request URL - url = urlparse(request.url) - # Parse the query parameters into a Python dictionary - params = parse_qs(url.query) +class Default(WorkerEntrypoint): + async def fetch(self, request, env): + # Parse the incoming request URL + url = urlparse(request.url) + # Parse the query parameters into a Python dictionary + params = parse_qs(url.query) - if "name" in params: - greeting = "Hello there, {name}".format(name=params["name"][0]) - return Response(greeting) + if "name" in params: + greeting = "Hello there, {name}".format(name=params["name"][0]) + return Response(greeting) - if url.path == "/favicon.ico": - return Response("") + if url.path == "/favicon.ico": + return Response("") - return Response("Hello world!") + return Response("Hello world!") ``` ## Parse JSON from the incoming request ```python -from workers import Response +from workers import WorkerEntrypoint, Response -async def on_fetch(request): - name = (await request.json()).name - return Response("Hello, {name}".format(name=name)) +class Default(WorkerEntrypoint): + async def fetch(self, request): + name = (await request.json()).name + return Response("Hello, {name}".format(name=name)) ``` ## Emit logs from your Python Worker @@ -49,28 +51,29 @@ async def on_fetch(request): ```python # To use the JavaScript console APIs from js import console -from workers import Response +from workers import WorkerEntrypoint, Response # To use the native Python logging import logging -async def on_fetch(request): - # Use the console APIs from JavaScript - # https://developer.mozilla.org/en-US/docs/Web/API/console - console.log("console.log from Python!") +class Default(WorkerEntrypoint): + async def fetch(self, request): + # Use the console APIs from JavaScript + # https://developer.mozilla.org/en-US/docs/Web/API/console + console.log("console.log from Python!") - # Alternatively, use the native Python logger - logger = logging.getLogger(__name__) + # Alternatively, use the native Python logger + logger = logging.getLogger(__name__) - # The default level is warning. We can change that to info. - logging.basicConfig(level=logging.INFO) + # The default level is warning. We can change that to info. + logging.basicConfig(level=logging.INFO) - logger.error("error from Python!") - logger.info("info log from Python!") + logger.error("error from Python!") + logger.info("info log from Python!") - # Or just use print() - print("print() from Python!") + # Or just use print() + print("print() from Python!") - return Response("We're testing logging!") + return Response("We're testing logging!") ``` ## Publish to a Queue @@ -79,35 +82,37 @@ async def on_fetch(request): from js import Object from pyodide.ffi import to_js as _to_js -from workers import Response +from workers import WorkerEntrypoint, Response # to_js converts between Python dictionaries and JavaScript Objects def to_js(obj): return _to_js(obj, dict_converter=Object.fromEntries) -async def on_fetch(request, env): - # Bindings are available on the 'env' parameter - # https://developers.cloudflare.com/queues/ +class Default(WorkerEntrypoint): + async def fetch(self, request, env): + # Bindings are available on the 'env' parameter + # https://developers.cloudflare.com/queues/ - # The default contentType is "json" - # We can also pass plain text strings - await env.QUEUE.send("hello", contentType="text") - # Send a JSON payload - await env.QUEUE.send(to_js({"hello": "world"})) + # The default contentType is "json" + # We can also pass plain text strings + await env.QUEUE.send("hello", contentType="text") + # Send a JSON payload + await env.QUEUE.send(to_js({"hello": "world"})) - # Return a response - return Response.json({"write": "success"}) + # Return a response + return Response.json({"write": "success"}) ``` ## Query a D1 Database ```python -from workers import Response +from workers import WorkerEntrypoint, Response -async def on_fetch(request, env): - results = await env.DB.prepare("PRAGMA table_list").run() - # Return a JSON response - return Response.json(results) +class Default(WorkerEntrypoint): + async def fetch(self, request, env): + results = await env.DB.prepare("PRAGMA table_list").run() + # Return a JSON response + return Response.json(results) ``` Refer to [Query D1 from Python Workers](/d1/examples/query-d1-from-python-workers/) for a more in-depth tutorial that covers how to create a new D1 database and configure bindings to D1. diff --git a/src/content/docs/workers/languages/python/ffi.mdx b/src/content/docs/workers/languages/python/ffi.mdx index f8ff1e67299a2d7..47262b2ef7a53c6 100644 --- a/src/content/docs/workers/languages/python/ffi.mdx +++ b/src/content/docs/workers/languages/python/ffi.mdx @@ -38,12 +38,13 @@ kv_namespaces = [ ...and then call `.get()` on the binding object that is exposed on `env`: ```python -from workers import Response +from workers import WorkerEntrypoint, Response -async def on_fetch(request, env): - await env.FOO.put("bar", "baz") - bar = await env.FOO.get("bar") - return Response(bar) # returns "baz" +class Default(WorkerEntrypoint): + async def fetch(self, request, env): + await env.FOO.put("bar", "baz") + bar = await env.FOO.get("bar") + return Response(bar) # returns "baz" ``` Under the hood, `env` is actually a JavaScript object. When you call `.FOO`, you are accessing this property via a [`JsProxy`](https://pyodide.org/en/stable/usage/api/python-api/ffi.html#pyodide.ffi.JsProxy) — special proxy object that makes a JavaScript object behave like a Python object. @@ -53,10 +54,12 @@ Under the hood, `env` is actually a JavaScript object. When you call `.FOO`, you When writing Workers in Python, you can access JavaScript globals by importing them from the `js` module. For example, note how `Response` is imported from `js` in the example below: ```python +from workers import WorkerEntrypoint from js import Response -def on_fetch(request): - return Response.new("Hello World!") +class Default(WorkerEntrypoint): + async def fetch(self, request): + return Response.new("Hello World!") ``` Refer to the [Python examples](/workers/languages/python/examples/) to learn how to call into JavaScript functions from Python, including `console.log` and logging, providing options to `Response`, and parsing JSON. diff --git a/src/content/docs/workers/languages/python/how-python-workers-work.mdx b/src/content/docs/workers/languages/python/how-python-workers-work.mdx index 2318c4cce0d3f15..a5abc278bd3c267 100644 --- a/src/content/docs/workers/languages/python/how-python-workers-work.mdx +++ b/src/content/docs/workers/languages/python/how-python-workers-work.mdx @@ -18,10 +18,11 @@ When you write a Python Worker, your code is interpreted directly by Pyodide, wi ## Local Development Lifecycle ```python -from workers import Response +from workers import Response, WorkerEntrypoint -async def on_fetch(request, env): - return Response("Hello world!") +class Default(WorkerEntrypoint): + async def fetch(self, request): + return Response("Hello world!") ``` …with a [Wrangler configuration file](/workers/wrangler/configuration/) that points to a .py file: diff --git a/src/content/docs/workers/languages/python/index.mdx b/src/content/docs/workers/languages/python/index.mdx index e0079808c034860..c5b386d15f0cf53 100644 --- a/src/content/docs/workers/languages/python/index.mdx +++ b/src/content/docs/workers/languages/python/index.mdx @@ -39,13 +39,14 @@ npx wrangler@latest dev A Python Worker can be as simple as three lines of code: ```python -from workers import Response +from workers import WorkerEntrypoint, Response -def on_fetch(request): - return Response("Hello World!") +class Default(WorkerEntrypoint): + async def fetch(self, request): + return Response("Hello World!") ``` -Similar to Workers written in [JavaScript](/workers/languages/javascript), [TypeScript](/workers/languages/typescript), or [Rust](/workers/languages/rust/), the main entry point for a Python worker is the [`fetch` handler](/workers/runtime-apis/handlers/fetch). In a Python Worker, this handler is named `on_fetch`. +Similar to Workers written in [JavaScript](/workers/languages/javascript), [TypeScript](/workers/languages/typescript), or [Rust](/workers/languages/rust/), the main entry point for a Python worker is the [`fetch` handler](/workers/runtime-apis/handlers/fetch). In a Python Worker, this handler is placed in a `Default` class that extends the `WorkerEntrypoint` class (which you can import from the `workers` SDK module). To run a Python Worker locally, you use [Wrangler](/workers/wrangler/), the CLI for Cloudflare Workers: @@ -72,10 +73,11 @@ Now, we can modify `src/entry.py` to make use of the new module. ```python from hello import hello -from workers import Response +from workers import WorkerEntrypoint, Response -def on_fetch(request): - return Response(hello("World")) +class Default(WorkerEntrypoint): + async def fetch(self, request): + return Response(hello("World")) ``` Once you edit `src/entry.py`, Wrangler will automatically detect the change and @@ -91,12 +93,13 @@ Let's try editing the worker to accept a POST request. We know from the JSON. In a Python Worker, you would write: ```python -from workers import Response +from workers import WorkerEntrypoint, Response from hello import hello -async def on_fetch(request): - name = (await request.json()).name - return Response(hello(name)) +class Default(WorkerEntrypoint): + async def fetch(self, request): + name = (await request.json()).name + return Response(hello(name)) ``` Once you edit the `src/entry.py`, Wrangler should automatically restart the local @@ -140,10 +143,11 @@ API_HOST = "example.com" Then, you can access the `API_HOST` environment variable via the `env` parameter: ```python -from workers import Response +from workers import WorkerEntrypoint, Response -async def on_fetch(request, env): - return Response(env.API_HOST) +class Default(WorkerEntrypoint): + async def fetch(self, request, env): + return Response(env.API_HOST) ``` ## Further Reading diff --git a/src/content/docs/workers/languages/python/packages/fastapi.mdx b/src/content/docs/workers/languages/python/packages/fastapi.mdx index b57cd40b4623050..ce935221f07fe5e 100644 --- a/src/content/docs/workers/languages/python/packages/fastapi.mdx +++ b/src/content/docs/workers/languages/python/packages/fastapi.mdx @@ -30,13 +30,15 @@ npx wrangler@latest dev ### Example code ```python +from workers import WorkerEntrypoint from fastapi import FastAPI, Request from pydantic import BaseModel -async def on_fetch(request, env): - import asgi +class Default(WorkerEntrypoint): + async def fetch(self, request, env): + import asgi - return await asgi.fetch(app, request, env) + return await asgi.fetch(app, request, env) app = FastAPI() diff --git a/src/content/docs/workers/languages/python/packages/langchain.mdx b/src/content/docs/workers/languages/python/packages/langchain.mdx index f67b14de02d15ad..c42e334b1039d12 100644 --- a/src/content/docs/workers/languages/python/packages/langchain.mdx +++ b/src/content/docs/workers/languages/python/packages/langchain.mdx @@ -32,15 +32,16 @@ npx wrangler@latest dev ### Example code ```python -from workers import Response +from workers import WorkerEntrypoint, Response from langchain_core.prompts import PromptTemplate from langchain_openai import OpenAI -async def on_fetch(request, env): - prompt = PromptTemplate.from_template("Complete the following sentence: I am a {profession} and ") - llm = OpenAI(api_key=env.API_KEY) - chain = prompt | llm +class Default(WorkerEntrypoint): + async def fetch(self, request, env): + prompt = PromptTemplate.from_template("Complete the following sentence: I am a {profession} and ") + llm = OpenAI(api_key=env.API_KEY) + chain = prompt | llm - res = await chain.ainvoke({"profession": "electrician"}) - return Response(res.split(".")[0].strip()) + res = await chain.ainvoke({"profession": "electrician"}) + return Response(res.split(".")[0].strip()) ``` diff --git a/src/content/docs/workers/platform/infrastructure-as-code.mdx b/src/content/docs/workers/platform/infrastructure-as-code.mdx index ff492fe82ab68cc..98ab9d6756a6753 100644 --- a/src/content/docs/workers/platform/infrastructure-as-code.mdx +++ b/src/content/docs/workers/platform/infrastructure-as-code.mdx @@ -155,10 +155,11 @@ curl https://api.cloudflare.com/client/v4/accounts//workers/scripts/ ] };type=application/json' \ -F 'my-hello-world-script.py=@-;filename=my-hello-world-script.py;type=text/x-python' < - + + + ```ts import { WorkerEntrypoint } from "cloudflare:workers"; @@ -33,6 +35,23 @@ export default class extends WorkerEntrypoint { + + +```python +from workers import WorkerEntrypoint, Response + +class Default(WorkerEntrypoint): + async def fetch(self, request, env, ctx): + return Response("Hello from Worker B") + + def add(self, a: int, b: int) -> int: + return a + b +``` + + + + + Worker A can declare a [binding](/workers/runtime-apis/bindings) to Worker B: @@ -49,7 +68,10 @@ services = [ Making it possible for Worker A to call the `add()` method from Worker B: - + + + + ```ts export default { @@ -61,3 +83,19 @@ export default { ``` + + + + +```python +from workers import WorkerEntrypoint, Response + +class Default(WorkerEntrypoint): + async def fetch(self, request, env, ctx): + result = await env.WORKER_B.add(1, 2) + return Response(f"Result: {result}") +``` + + + +