Add Lower Level (Complete) HTTP Request/Response API #56

Description

@posborne

Overview

Add low-level HTTP Request and Response wrappers for advanced use cases requiring direct control over HTTP primitives, streaming, and Fastly-specific features.

Context - What exists:

  • WSGI adapter - Run Flask/Bottle apps unmodified
  • requests facade - Client API for making backend calls (requests.get(), etc.)
  • Low-level Request/Response API - This issue

What this enables:

  • Streaming/proxying without buffering entire request/response bodies
  • Access to Fastly-specific metadata (TLS fingerprints, client IP, compliance region)
  • Request transformation (modify incoming request, send to backend)
  • Cache control and surrogate key management
  • Foundation for other SDK features (cache, security, image optimizer APIs)

When to use what:

Use CaseUse ThisNot This
Run Flask appWSGI adapterThis
Make API calls from apprequests.get()This
Proxy/stream requestsThis (Request/Response)requests facade
Need TLS/IP metadataThis (Request.downstream)WSGI
Cache override/surrogate keysThisrequests facade

WIT Interface

interfacehttp-req {
usetypes.{error};
usehttp-types.{http-version};
usehttp-resp.{response};
usehttp-body.{body};
usebackend.{backend};
resourcerequest {
new:static func() ->result<request, error>;
set-cache-override:func(cache-override:cache-override) ->result<_, error>;
get-header-names:func(max-len:u64, cursor:u32) ->result<tuple<string, option<u32>>, error>;
get-header-value:func(name:string, max-len:u64) ->result<option<list<u8>>, error>;
get-header-values:func(name:string, max-len:u64, cursor:u32) ->result<tuple<list<u8>, option<u32>>, error>;
set-header-values:func(name:string, values:list<u8>) ->result<_, error>;
get-method:func(max-len:u64) ->result<string, error>;
set-method:func(method:string) ->result<_, error>;
get-uri:func(max-len:u64) ->result<string, error>;
set-uri:func(uri:string) ->result<_, error>;
get-version:func() ->result<http-version, error>;
set-version:func(version:http-version) ->result<_, error>;
send:func(backend:borrow<backend>, body:body) ->result<response, error>;
}
}
interfacehttp-resp {
usetypes.{error};
usehttp-types.{http-version};
usehttp-body.{body};
resourceresponse {
new:static func() ->result<response, error>;
get-status:func() ->result<u16, error>;
set-status:func(status:u16) ->result<_, error>;
get-version:func() ->result<http-version, error>;
set-version:func(version:http-version) ->result<_, error>;
get-header-names:func(max-len:u64, cursor:u32) ->result<tuple<string, option<u32>>, error>;
get-header-value:func(name:string, max-len:u64) ->result<option<list<u8>>, error>;
get-header-values:func(name:string, max-len:u64, cursor:u32) ->result<tuple<list<u8>, option<u32>>, error>;
set-header-values:func(name:string, values:list<u8>) ->result<_, error>;
send-downstream:func(body:body, streaming:bool) ->result<_, error>;
}
}
interfacehttp-downstream {
usetypes.{ip-address, error};
usehttp-req.{request};
downstream-client-ip-addr:func(ds-request:borrow<request>) ->option<ip-address>;
downstream-server-ip-addr:func(ds-request:borrow<request>) ->option<ip-address>;
downstream-client-request-id:func(ds-request:borrow<request>, max-len:u64) ->result<string, error>;
downstream-tls-cipher-openssl-name:func(ds-request:borrow<request>, max-len:u64) ->result<option<list<u8>>, error>;
downstream-tls-protocol:func(ds-request:borrow<request>, max-len:u64) ->result<option<list<u8>>, error>;
downstream-tls-ja3-md5:func(ds-request:borrow<request>) ->result<option<list<u8>>, error>;
downstream-tls-ja4:func(ds-request:borrow<request>, max-len:u64) ->result<option<string>, error>;
}

WIT bindings: stubs/wit_world/imports/http_req.py, http_resp.py, http_downstream.py, http_body.py

API Design

Core types:

  • Request - Wraps http_req.Request with Pythonic API (properties for method, uri, version)
  • Response - Wraps http_resp.Response
  • Headers - Dict-like interface for header manipulation
  • Body - io.IOBase-compatible for streaming (use shutil.copyfileobj(), etc.)

Key features:

  • Downstream metadata via request.downstream accessor:
    • client_ip()IPv4Address | IPv6Address
    • tls_cipher(), tls_ja3_md5(), tls_ja4() → TLS fingerprints
    • compliance_region() → GDPR/data residency region
  • Cache control: request.set_cache_override(ttl=..., surrogate_key=...)
  • Backend requests: request.send(backend, body)Response
  • Streaming: Bodies are file-like objects, work with stdlib

Example - Proxying with transformation:

defhandle(incoming_req, incoming_body):
# Access metadataclient_ip=incoming_req.downstream.client_ip()
# Transform requestincoming_req.headers['X-Forwarded-For'] =str(client_ip)
incoming_req.set_cache_override(ttl=3600, surrogate_key='user-data')
# Send to backend (streaming)response=incoming_req.send('origin', incoming_body)
returnresponse

Integration with Existing SDK

WSGI Adapter

Can wrap incoming WIT request in Request object and expose via environ['fastly.request']:

fromflaskimportFlask, request@app.route("/api/data")defget_data():
fastly_req=request.environ['fastly.request']
client_ip=fastly_req.downstream.client_ip()
return {"client_ip": str(client_ip)}

Requests Facade

The requests.get() / requests.post() API is for making outgoing requests (client use case). It should NOT be extended for proxying - that's what this low-level API is for.

Clear separation:

  • Client pattern (outgoing): Use requests.get(url) - builds request from scratch
  • Server/Proxy pattern (incoming): Use Request/Response API - transforms received request

The requests facade could use Request wrappers internally but public API stays the same.

Cross-SDK Comparison:

  • Rust: Request/Response types wrapping HTTP standard types. Methods for headers, body streams, methods, URLs. Rich builder patterns. Strongly typed.
  • Go: Standard *http.Request/*http.Response from stdlib with Fastly extensions via embedded fields/methods.
  • JS: Standard Request/Response from Fetch API with Fastly extensions.

Recommended approach: Python should provide standard library-compatible types (similar to requests or urllib) while adding Fastly-specific extensions.

Viceroy Testing

Viceroy supports HTTP request/response handling with full metadata access in tests. The @on_viceroy decorator can provide synthetic requests with headers, bodies, and metadata.

Tests can verify:

  • Request/response creation and manipulation
  • Header handling (case-insensitive, multi-value)
  • Body streaming and reading
  • Downstream metadata access (may have defaults for TLS info in Viceroy)

HTTP operations are well-supported in Viceroy testing.

Reference

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
       blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
      }
      } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
      })();
      (function(){
      try {
      var __m = "github.com";
      var __re = new RegExp('^' + "github\\.com" + '
      
      Skip to content

      Add Lower Level (Complete) HTTP Request/Response API #56

      Description

      @posborne

      Overview

      Add low-level HTTP Request and Response wrappers for advanced use cases requiring direct control over HTTP primitives, streaming, and Fastly-specific features.

      Context - What exists:

      • WSGI adapter - Run Flask/Bottle apps unmodified
      • requests facade - Client API for making backend calls (requests.get(), etc.)
      • Low-level Request/Response API - This issue

      What this enables:

      • Streaming/proxying without buffering entire request/response bodies
      • Access to Fastly-specific metadata (TLS fingerprints, client IP, compliance region)
      • Request transformation (modify incoming request, send to backend)
      • Cache control and surrogate key management
      • Foundation for other SDK features (cache, security, image optimizer APIs)

      When to use what:

      Use CaseUse ThisNot This
      Run Flask appWSGI adapterThis
      Make API calls from apprequests.get()This
      Proxy/stream requestsThis (Request/Response)requests facade
      Need TLS/IP metadataThis (Request.downstream)WSGI
      Cache override/surrogate keysThisrequests facade

      WIT Interface

      interfacehttp-req {
      usetypes.{error};
      usehttp-types.{http-version};
      usehttp-resp.{response};
      usehttp-body.{body};
      usebackend.{backend};
      resourcerequest {
      new:static func() ->result<request, error>;
      set-cache-override:func(cache-override:cache-override) ->result<_, error>;
      get-header-names:func(max-len:u64, cursor:u32) ->result<tuple<string, option<u32>>, error>;
      get-header-value:func(name:string, max-len:u64) ->result<option<list<u8>>, error>;
      get-header-values:func(name:string, max-len:u64, cursor:u32) ->result<tuple<list<u8>, option<u32>>, error>;
      set-header-values:func(name:string, values:list<u8>) ->result<_, error>;
      get-method:func(max-len:u64) ->result<string, error>;
      set-method:func(method:string) ->result<_, error>;
      get-uri:func(max-len:u64) ->result<string, error>;
      set-uri:func(uri:string) ->result<_, error>;
      get-version:func() ->result<http-version, error>;
      set-version:func(version:http-version) ->result<_, error>;
      send:func(backend:borrow<backend>, body:body) ->result<response, error>;
      }
      }
      interfacehttp-resp {
      usetypes.{error};
      usehttp-types.{http-version};
      usehttp-body.{body};
      resourceresponse {
      new:static func() ->result<response, error>;
      get-status:func() ->result<u16, error>;
      set-status:func(status:u16) ->result<_, error>;
      get-version:func() ->result<http-version, error>;
      set-version:func(version:http-version) ->result<_, error>;
      get-header-names:func(max-len:u64, cursor:u32) ->result<tuple<string, option<u32>>, error>;
      get-header-value:func(name:string, max-len:u64) ->result<option<list<u8>>, error>;
      get-header-values:func(name:string, max-len:u64, cursor:u32) ->result<tuple<list<u8>, option<u32>>, error>;
      set-header-values:func(name:string, values:list<u8>) ->result<_, error>;
      send-downstream:func(body:body, streaming:bool) ->result<_, error>;
      }
      }
      interfacehttp-downstream {
      usetypes.{ip-address, error};
      usehttp-req.{request};
      downstream-client-ip-addr:func(ds-request:borrow<request>) ->option<ip-address>;
      downstream-server-ip-addr:func(ds-request:borrow<request>) ->option<ip-address>;
      downstream-client-request-id:func(ds-request:borrow<request>, max-len:u64) ->result<string, error>;
      downstream-tls-cipher-openssl-name:func(ds-request:borrow<request>, max-len:u64) ->result<option<list<u8>>, error>;
      downstream-tls-protocol:func(ds-request:borrow<request>, max-len:u64) ->result<option<list<u8>>, error>;
      downstream-tls-ja3-md5:func(ds-request:borrow<request>) ->result<option<list<u8>>, error>;
      downstream-tls-ja4:func(ds-request:borrow<request>, max-len:u64) ->result<option<string>, error>;
      }

      WIT bindings: stubs/wit_world/imports/http_req.py, http_resp.py, http_downstream.py, http_body.py

      API Design

      Core types:

      • Request - Wraps http_req.Request with Pythonic API (properties for method, uri, version)
      • Response - Wraps http_resp.Response
      • Headers - Dict-like interface for header manipulation
      • Body - io.IOBase-compatible for streaming (use shutil.copyfileobj(), etc.)

      Key features:

      • Downstream metadata via request.downstream accessor:
        • client_ip()IPv4Address | IPv6Address
        • tls_cipher(), tls_ja3_md5(), tls_ja4() → TLS fingerprints
        • compliance_region() → GDPR/data residency region
      • Cache control: request.set_cache_override(ttl=..., surrogate_key=...)
      • Backend requests: request.send(backend, body)Response
      • Streaming: Bodies are file-like objects, work with stdlib

      Example - Proxying with transformation:

      defhandle(incoming_req, incoming_body):
      # Access metadataclient_ip=incoming_req.downstream.client_ip()
      # Transform requestincoming_req.headers['X-Forwarded-For'] =str(client_ip)
      incoming_req.set_cache_override(ttl=3600, surrogate_key='user-data')
      # Send to backend (streaming)response=incoming_req.send('origin', incoming_body)
      returnresponse

      Integration with Existing SDK

      WSGI Adapter

      Can wrap incoming WIT request in Request object and expose via environ['fastly.request']:

      fromflaskimportFlask, request@app.route("/api/data")defget_data():
      fastly_req=request.environ['fastly.request']
      client_ip=fastly_req.downstream.client_ip()
      return {"client_ip": str(client_ip)}

      Requests Facade

      The requests.get() / requests.post() API is for making outgoing requests (client use case). It should NOT be extended for proxying - that's what this low-level API is for.

      Clear separation:

      • Client pattern (outgoing): Use requests.get(url) - builds request from scratch
      • Server/Proxy pattern (incoming): Use Request/Response API - transforms received request

      The requests facade could use Request wrappers internally but public API stays the same.

      Cross-SDK Comparison:

      • Rust: Request/Response types wrapping HTTP standard types. Methods for headers, body streams, methods, URLs. Rich builder patterns. Strongly typed.
      • Go: Standard *http.Request/*http.Response from stdlib with Fastly extensions via embedded fields/methods.
      • JS: Standard Request/Response from Fetch API with Fastly extensions.

      Recommended approach: Python should provide standard library-compatible types (similar to requests or urllib) while adding Fastly-specific extensions.

      Viceroy Testing

      Viceroy supports HTTP request/response handling with full metadata access in tests. The @on_viceroy decorator can provide synthetic requests with headers, bodies, and metadata.

      Tests can verify:

      • Request/response creation and manipulation
      • Header handling (case-insensitive, multi-value)
      • Body streaming and reading
      • Downstream metadata access (may have defaults for TLS info in Viceroy)

      HTTP operations are well-supported in Viceroy testing.

      Reference

      Activity

      Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

      Metadata

      Metadata

      Assignees

      No one assigned

        Labels

        No labels
        No labels

        Type

        No type

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
          Skip to content

          Add Lower Level (Complete) HTTP Request/Response API #56

          Description

          @posborne

          Overview

          Add low-level HTTP Request and Response wrappers for advanced use cases requiring direct control over HTTP primitives, streaming, and Fastly-specific features.

          Context - What exists:

          • WSGI adapter - Run Flask/Bottle apps unmodified
          • requests facade - Client API for making backend calls (requests.get(), etc.)
          • Low-level Request/Response API - This issue

          What this enables:

          • Streaming/proxying without buffering entire request/response bodies
          • Access to Fastly-specific metadata (TLS fingerprints, client IP, compliance region)
          • Request transformation (modify incoming request, send to backend)
          • Cache control and surrogate key management
          • Foundation for other SDK features (cache, security, image optimizer APIs)

          When to use what:

          Use CaseUse ThisNot This
          Run Flask appWSGI adapterThis
          Make API calls from apprequests.get()This
          Proxy/stream requestsThis (Request/Response)requests facade
          Need TLS/IP metadataThis (Request.downstream)WSGI
          Cache override/surrogate keysThisrequests facade

          WIT Interface

          interfacehttp-req {
          usetypes.{error};
          usehttp-types.{http-version};
          usehttp-resp.{response};
          usehttp-body.{body};
          usebackend.{backend};
          resourcerequest {
          new:static func() ->result<request, error>;
          set-cache-override:func(cache-override:cache-override) ->result<_, error>;
          get-header-names:func(max-len:u64, cursor:u32) ->result<tuple<string, option<u32>>, error>;
          get-header-value:func(name:string, max-len:u64) ->result<option<list<u8>>, error>;
          get-header-values:func(name:string, max-len:u64, cursor:u32) ->result<tuple<list<u8>, option<u32>>, error>;
          set-header-values:func(name:string, values:list<u8>) ->result<_, error>;
          get-method:func(max-len:u64) ->result<string, error>;
          set-method:func(method:string) ->result<_, error>;
          get-uri:func(max-len:u64) ->result<string, error>;
          set-uri:func(uri:string) ->result<_, error>;
          get-version:func() ->result<http-version, error>;
          set-version:func(version:http-version) ->result<_, error>;
          send:func(backend:borrow<backend>, body:body) ->result<response, error>;
          }
          }
          interfacehttp-resp {
          usetypes.{error};
          usehttp-types.{http-version};
          usehttp-body.{body};
          resourceresponse {
          new:static func() ->result<response, error>;
          get-status:func() ->result<u16, error>;
          set-status:func(status:u16) ->result<_, error>;
          get-version:func() ->result<http-version, error>;
          set-version:func(version:http-version) ->result<_, error>;
          get-header-names:func(max-len:u64, cursor:u32) ->result<tuple<string, option<u32>>, error>;
          get-header-value:func(name:string, max-len:u64) ->result<option<list<u8>>, error>;
          get-header-values:func(name:string, max-len:u64, cursor:u32) ->result<tuple<list<u8>, option<u32>>, error>;
          set-header-values:func(name:string, values:list<u8>) ->result<_, error>;
          send-downstream:func(body:body, streaming:bool) ->result<_, error>;
          }
          }
          interfacehttp-downstream {
          usetypes.{ip-address, error};
          usehttp-req.{request};
          downstream-client-ip-addr:func(ds-request:borrow<request>) ->option<ip-address>;
          downstream-server-ip-addr:func(ds-request:borrow<request>) ->option<ip-address>;
          downstream-client-request-id:func(ds-request:borrow<request>, max-len:u64) ->result<string, error>;
          downstream-tls-cipher-openssl-name:func(ds-request:borrow<request>, max-len:u64) ->result<option<list<u8>>, error>;
          downstream-tls-protocol:func(ds-request:borrow<request>, max-len:u64) ->result<option<list<u8>>, error>;
          downstream-tls-ja3-md5:func(ds-request:borrow<request>) ->result<option<list<u8>>, error>;
          downstream-tls-ja4:func(ds-request:borrow<request>, max-len:u64) ->result<option<string>, error>;
          }

          WIT bindings: stubs/wit_world/imports/http_req.py, http_resp.py, http_downstream.py, http_body.py

          API Design

          Core types:

          • Request - Wraps http_req.Request with Pythonic API (properties for method, uri, version)
          • Response - Wraps http_resp.Response
          • Headers - Dict-like interface for header manipulation
          • Body - io.IOBase-compatible for streaming (use shutil.copyfileobj(), etc.)

          Key features:

          • Downstream metadata via request.downstream accessor:
            • client_ip()IPv4Address | IPv6Address
            • tls_cipher(), tls_ja3_md5(), tls_ja4() → TLS fingerprints
            • compliance_region() → GDPR/data residency region
          • Cache control: request.set_cache_override(ttl=..., surrogate_key=...)
          • Backend requests: request.send(backend, body)Response
          • Streaming: Bodies are file-like objects, work with stdlib

          Example - Proxying with transformation:

          defhandle(incoming_req, incoming_body):
          # Access metadataclient_ip=incoming_req.downstream.client_ip()
          # Transform requestincoming_req.headers['X-Forwarded-For'] =str(client_ip)
          incoming_req.set_cache_override(ttl=3600, surrogate_key='user-data')
          # Send to backend (streaming)response=incoming_req.send('origin', incoming_body)
          returnresponse

          Integration with Existing SDK

          WSGI Adapter

          Can wrap incoming WIT request in Request object and expose via environ['fastly.request']:

          fromflaskimportFlask, request@app.route("/api/data")defget_data():
          fastly_req=request.environ['fastly.request']
          client_ip=fastly_req.downstream.client_ip()
          return {"client_ip": str(client_ip)}

          Requests Facade

          The requests.get() / requests.post() API is for making outgoing requests (client use case). It should NOT be extended for proxying - that's what this low-level API is for.

          Clear separation:

          • Client pattern (outgoing): Use requests.get(url) - builds request from scratch
          • Server/Proxy pattern (incoming): Use Request/Response API - transforms received request

          The requests facade could use Request wrappers internally but public API stays the same.

          Cross-SDK Comparison:

          • Rust: Request/Response types wrapping HTTP standard types. Methods for headers, body streams, methods, URLs. Rich builder patterns. Strongly typed.
          • Go: Standard *http.Request/*http.Response from stdlib with Fastly extensions via embedded fields/methods.
          • JS: Standard Request/Response from Fetch API with Fastly extensions.

          Recommended approach: Python should provide standard library-compatible types (similar to requests or urllib) while adding Fastly-specific extensions.

          Viceroy Testing

          Viceroy supports HTTP request/response handling with full metadata access in tests. The @on_viceroy decorator can provide synthetic requests with headers, bodies, and metadata.

          Tests can verify:

          • Request/response creation and manipulation
          • Header handling (case-insensitive, multi-value)
          • Body streaming and reading
          • Downstream metadata access (may have defaults for TLS info in Viceroy)

          HTTP operations are well-supported in Viceroy testing.

          Reference

          Activity

          Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

          Metadata

          Metadata

          Assignees

          No one assigned

            Labels

            No labels
            No labels

            Type

            No type

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              Skip to content

              Add Lower Level (Complete) HTTP Request/Response API #56

              Description

              @posborne

              Overview

              Add low-level HTTP Request and Response wrappers for advanced use cases requiring direct control over HTTP primitives, streaming, and Fastly-specific features.

              Context - What exists:

              • WSGI adapter - Run Flask/Bottle apps unmodified
              • requests facade - Client API for making backend calls (requests.get(), etc.)
              • Low-level Request/Response API - This issue

              What this enables:

              • Streaming/proxying without buffering entire request/response bodies
              • Access to Fastly-specific metadata (TLS fingerprints, client IP, compliance region)
              • Request transformation (modify incoming request, send to backend)
              • Cache control and surrogate key management
              • Foundation for other SDK features (cache, security, image optimizer APIs)

              When to use what:

              Use CaseUse ThisNot This
              Run Flask appWSGI adapterThis
              Make API calls from apprequests.get()This
              Proxy/stream requestsThis (Request/Response)requests facade
              Need TLS/IP metadataThis (Request.downstream)WSGI
              Cache override/surrogate keysThisrequests facade

              WIT Interface

              interfacehttp-req {
              usetypes.{error};
              usehttp-types.{http-version};
              usehttp-resp.{response};
              usehttp-body.{body};
              usebackend.{backend};
              resourcerequest {
              new:static func() ->result<request, error>;
              set-cache-override:func(cache-override:cache-override) ->result<_, error>;
              get-header-names:func(max-len:u64, cursor:u32) ->result<tuple<string, option<u32>>, error>;
              get-header-value:func(name:string, max-len:u64) ->result<option<list<u8>>, error>;
              get-header-values:func(name:string, max-len:u64, cursor:u32) ->result<tuple<list<u8>, option<u32>>, error>;
              set-header-values:func(name:string, values:list<u8>) ->result<_, error>;
              get-method:func(max-len:u64) ->result<string, error>;
              set-method:func(method:string) ->result<_, error>;
              get-uri:func(max-len:u64) ->result<string, error>;
              set-uri:func(uri:string) ->result<_, error>;
              get-version:func() ->result<http-version, error>;
              set-version:func(version:http-version) ->result<_, error>;
              send:func(backend:borrow<backend>, body:body) ->result<response, error>;
              }
              }
              interfacehttp-resp {
              usetypes.{error};
              usehttp-types.{http-version};
              usehttp-body.{body};
              resourceresponse {
              new:static func() ->result<response, error>;
              get-status:func() ->result<u16, error>;
              set-status:func(status:u16) ->result<_, error>;
              get-version:func() ->result<http-version, error>;
              set-version:func(version:http-version) ->result<_, error>;
              get-header-names:func(max-len:u64, cursor:u32) ->result<tuple<string, option<u32>>, error>;
              get-header-value:func(name:string, max-len:u64) ->result<option<list<u8>>, error>;
              get-header-values:func(name:string, max-len:u64, cursor:u32) ->result<tuple<list<u8>, option<u32>>, error>;
              set-header-values:func(name:string, values:list<u8>) ->result<_, error>;
              send-downstream:func(body:body, streaming:bool) ->result<_, error>;
              }
              }
              interfacehttp-downstream {
              usetypes.{ip-address, error};
              usehttp-req.{request};
              downstream-client-ip-addr:func(ds-request:borrow<request>) ->option<ip-address>;
              downstream-server-ip-addr:func(ds-request:borrow<request>) ->option<ip-address>;
              downstream-client-request-id:func(ds-request:borrow<request>, max-len:u64) ->result<string, error>;
              downstream-tls-cipher-openssl-name:func(ds-request:borrow<request>, max-len:u64) ->result<option<list<u8>>, error>;
              downstream-tls-protocol:func(ds-request:borrow<request>, max-len:u64) ->result<option<list<u8>>, error>;
              downstream-tls-ja3-md5:func(ds-request:borrow<request>) ->result<option<list<u8>>, error>;
              downstream-tls-ja4:func(ds-request:borrow<request>, max-len:u64) ->result<option<string>, error>;
              }

              WIT bindings: stubs/wit_world/imports/http_req.py, http_resp.py, http_downstream.py, http_body.py

              API Design

              Core types:

              • Request - Wraps http_req.Request with Pythonic API (properties for method, uri, version)
              • Response - Wraps http_resp.Response
              • Headers - Dict-like interface for header manipulation
              • Body - io.IOBase-compatible for streaming (use shutil.copyfileobj(), etc.)

              Key features:

              • Downstream metadata via request.downstream accessor:
                • client_ip()IPv4Address | IPv6Address
                • tls_cipher(), tls_ja3_md5(), tls_ja4() → TLS fingerprints
                • compliance_region() → GDPR/data residency region
              • Cache control: request.set_cache_override(ttl=..., surrogate_key=...)
              • Backend requests: request.send(backend, body)Response
              • Streaming: Bodies are file-like objects, work with stdlib

              Example - Proxying with transformation:

              defhandle(incoming_req, incoming_body):
              # Access metadataclient_ip=incoming_req.downstream.client_ip()
              # Transform requestincoming_req.headers['X-Forwarded-For'] =str(client_ip)
              incoming_req.set_cache_override(ttl=3600, surrogate_key='user-data')
              # Send to backend (streaming)response=incoming_req.send('origin', incoming_body)
              returnresponse

              Integration with Existing SDK

              WSGI Adapter

              Can wrap incoming WIT request in Request object and expose via environ['fastly.request']:

              fromflaskimportFlask, request@app.route("/api/data")defget_data():
              fastly_req=request.environ['fastly.request']
              client_ip=fastly_req.downstream.client_ip()
              return {"client_ip": str(client_ip)}

              Requests Facade

              The requests.get() / requests.post() API is for making outgoing requests (client use case). It should NOT be extended for proxying - that's what this low-level API is for.

              Clear separation:

              • Client pattern (outgoing): Use requests.get(url) - builds request from scratch
              • Server/Proxy pattern (incoming): Use Request/Response API - transforms received request

              The requests facade could use Request wrappers internally but public API stays the same.

              Cross-SDK Comparison:

              • Rust: Request/Response types wrapping HTTP standard types. Methods for headers, body streams, methods, URLs. Rich builder patterns. Strongly typed.
              • Go: Standard *http.Request/*http.Response from stdlib with Fastly extensions via embedded fields/methods.
              • JS: Standard Request/Response from Fetch API with Fastly extensions.

              Recommended approach: Python should provide standard library-compatible types (similar to requests or urllib) while adding Fastly-specific extensions.

              Viceroy Testing

              Viceroy supports HTTP request/response handling with full metadata access in tests. The @on_viceroy decorator can provide synthetic requests with headers, bodies, and metadata.

              Tests can verify:

              • Request/response creation and manipulation
              • Header handling (case-insensitive, multi-value)
              • Body streaming and reading
              • Downstream metadata access (may have defaults for TLS info in Viceroy)

              HTTP operations are well-supported in Viceroy testing.

              Reference

              Activity

              Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

              Metadata

              Metadata

              Assignees

              No one assigned

                Labels

                No labels
                No labels

                Type

                No type

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions

                  , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
                  Skip to content

                  Add Lower Level (Complete) HTTP Request/Response API #56

                  Description

                  @posborne

                  Overview

                  Add low-level HTTP Request and Response wrappers for advanced use cases requiring direct control over HTTP primitives, streaming, and Fastly-specific features.

                  Context - What exists:

                  • WSGI adapter - Run Flask/Bottle apps unmodified
                  • requests facade - Client API for making backend calls (requests.get(), etc.)
                  • Low-level Request/Response API - This issue

                  What this enables:

                  • Streaming/proxying without buffering entire request/response bodies
                  • Access to Fastly-specific metadata (TLS fingerprints, client IP, compliance region)
                  • Request transformation (modify incoming request, send to backend)
                  • Cache control and surrogate key management
                  • Foundation for other SDK features (cache, security, image optimizer APIs)

                  When to use what:

                  Use CaseUse ThisNot This
                  Run Flask appWSGI adapterThis
                  Make API calls from apprequests.get()This
                  Proxy/stream requestsThis (Request/Response)requests facade
                  Need TLS/IP metadataThis (Request.downstream)WSGI
                  Cache override/surrogate keysThisrequests facade

                  WIT Interface

                  interfacehttp-req {
                  usetypes.{error};
                  usehttp-types.{http-version};
                  usehttp-resp.{response};
                  usehttp-body.{body};
                  usebackend.{backend};
                  resourcerequest {
                  new:static func() ->result<request, error>;
                  set-cache-override:func(cache-override:cache-override) ->result<_, error>;
                  get-header-names:func(max-len:u64, cursor:u32) ->result<tuple<string, option<u32>>, error>;
                  get-header-value:func(name:string, max-len:u64) ->result<option<list<u8>>, error>;
                  get-header-values:func(name:string, max-len:u64, cursor:u32) ->result<tuple<list<u8>, option<u32>>, error>;
                  set-header-values:func(name:string, values:list<u8>) ->result<_, error>;
                  get-method:func(max-len:u64) ->result<string, error>;
                  set-method:func(method:string) ->result<_, error>;
                  get-uri:func(max-len:u64) ->result<string, error>;
                  set-uri:func(uri:string) ->result<_, error>;
                  get-version:func() ->result<http-version, error>;
                  set-version:func(version:http-version) ->result<_, error>;
                  send:func(backend:borrow<backend>, body:body) ->result<response, error>;
                  }
                  }
                  interfacehttp-resp {
                  usetypes.{error};
                  usehttp-types.{http-version};
                  usehttp-body.{body};
                  resourceresponse {
                  new:static func() ->result<response, error>;
                  get-status:func() ->result<u16, error>;
                  set-status:func(status:u16) ->result<_, error>;
                  get-version:func() ->result<http-version, error>;
                  set-version:func(version:http-version) ->result<_, error>;
                  get-header-names:func(max-len:u64, cursor:u32) ->result<tuple<string, option<u32>>, error>;
                  get-header-value:func(name:string, max-len:u64) ->result<option<list<u8>>, error>;
                  get-header-values:func(name:string, max-len:u64, cursor:u32) ->result<tuple<list<u8>, option<u32>>, error>;
                  set-header-values:func(name:string, values:list<u8>) ->result<_, error>;
                  send-downstream:func(body:body, streaming:bool) ->result<_, error>;
                  }
                  }
                  interfacehttp-downstream {
                  usetypes.{ip-address, error};
                  usehttp-req.{request};
                  downstream-client-ip-addr:func(ds-request:borrow<request>) ->option<ip-address>;
                  downstream-server-ip-addr:func(ds-request:borrow<request>) ->option<ip-address>;
                  downstream-client-request-id:func(ds-request:borrow<request>, max-len:u64) ->result<string, error>;
                  downstream-tls-cipher-openssl-name:func(ds-request:borrow<request>, max-len:u64) ->result<option<list<u8>>, error>;
                  downstream-tls-protocol:func(ds-request:borrow<request>, max-len:u64) ->result<option<list<u8>>, error>;
                  downstream-tls-ja3-md5:func(ds-request:borrow<request>) ->result<option<list<u8>>, error>;
                  downstream-tls-ja4:func(ds-request:borrow<request>, max-len:u64) ->result<option<string>, error>;
                  }

                  WIT bindings: stubs/wit_world/imports/http_req.py, http_resp.py, http_downstream.py, http_body.py

                  API Design

                  Core types:

                  • Request - Wraps http_req.Request with Pythonic API (properties for method, uri, version)
                  • Response - Wraps http_resp.Response
                  • Headers - Dict-like interface for header manipulation
                  • Body - io.IOBase-compatible for streaming (use shutil.copyfileobj(), etc.)

                  Key features:

                  • Downstream metadata via request.downstream accessor:
                    • client_ip()IPv4Address | IPv6Address
                    • tls_cipher(), tls_ja3_md5(), tls_ja4() → TLS fingerprints
                    • compliance_region() → GDPR/data residency region
                  • Cache control: request.set_cache_override(ttl=..., surrogate_key=...)
                  • Backend requests: request.send(backend, body)Response
                  • Streaming: Bodies are file-like objects, work with stdlib

                  Example - Proxying with transformation:

                  defhandle(incoming_req, incoming_body):
                  # Access metadataclient_ip=incoming_req.downstream.client_ip()
                  # Transform requestincoming_req.headers['X-Forwarded-For'] =str(client_ip)
                  incoming_req.set_cache_override(ttl=3600, surrogate_key='user-data')
                  # Send to backend (streaming)response=incoming_req.send('origin', incoming_body)
                  returnresponse

                  Integration with Existing SDK

                  WSGI Adapter

                  Can wrap incoming WIT request in Request object and expose via environ['fastly.request']:

                  fromflaskimportFlask, request@app.route("/api/data")defget_data():
                  fastly_req=request.environ['fastly.request']
                  client_ip=fastly_req.downstream.client_ip()
                  return {"client_ip": str(client_ip)}

                  Requests Facade

                  The requests.get() / requests.post() API is for making outgoing requests (client use case). It should NOT be extended for proxying - that's what this low-level API is for.

                  Clear separation:

                  • Client pattern (outgoing): Use requests.get(url) - builds request from scratch
                  • Server/Proxy pattern (incoming): Use Request/Response API - transforms received request

                  The requests facade could use Request wrappers internally but public API stays the same.

                  Cross-SDK Comparison:

                  • Rust: Request/Response types wrapping HTTP standard types. Methods for headers, body streams, methods, URLs. Rich builder patterns. Strongly typed.
                  • Go: Standard *http.Request/*http.Response from stdlib with Fastly extensions via embedded fields/methods.
                  • JS: Standard Request/Response from Fetch API with Fastly extensions.

                  Recommended approach: Python should provide standard library-compatible types (similar to requests or urllib) while adding Fastly-specific extensions.

                  Viceroy Testing

                  Viceroy supports HTTP request/response handling with full metadata access in tests. The @on_viceroy decorator can provide synthetic requests with headers, bodies, and metadata.

                  Tests can verify:

                  • Request/response creation and manipulation
                  • Header handling (case-insensitive, multi-value)
                  • Body streaming and reading
                  • Downstream metadata access (may have defaults for TLS info in Viceroy)

                  HTTP operations are well-supported in Viceroy testing.

                  Reference

                  Activity

                  Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

                  Metadata

                  Metadata

                  Assignees

                  No one assigned

                    Labels

                    No labels
                    No labels

                    Type

                    No type

                    Projects

                    No projects

                      Milestone

                      No milestone

                      Relationships

                      None yet

                      Development

                      No branches or pull requests

                      Issue actions

                      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                      Skip to content

                      Add Lower Level (Complete) HTTP Request/Response API #56

                      Description

                      @posborne

                      Overview

                      Add low-level HTTP Request and Response wrappers for advanced use cases requiring direct control over HTTP primitives, streaming, and Fastly-specific features.

                      Context - What exists:

                      • WSGI adapter - Run Flask/Bottle apps unmodified
                      • requests facade - Client API for making backend calls (requests.get(), etc.)
                      • Low-level Request/Response API - This issue

                      What this enables:

                      • Streaming/proxying without buffering entire request/response bodies
                      • Access to Fastly-specific metadata (TLS fingerprints, client IP, compliance region)
                      • Request transformation (modify incoming request, send to backend)
                      • Cache control and surrogate key management
                      • Foundation for other SDK features (cache, security, image optimizer APIs)

                      When to use what:

                      Use CaseUse ThisNot This
                      Run Flask appWSGI adapterThis
                      Make API calls from apprequests.get()This
                      Proxy/stream requestsThis (Request/Response)requests facade
                      Need TLS/IP metadataThis (Request.downstream)WSGI
                      Cache override/surrogate keysThisrequests facade

                      WIT Interface

                      interfacehttp-req {
                      usetypes.{error};
                      usehttp-types.{http-version};
                      usehttp-resp.{response};
                      usehttp-body.{body};
                      usebackend.{backend};
                      resourcerequest {
                      new:static func() ->result<request, error>;
                      set-cache-override:func(cache-override:cache-override) ->result<_, error>;
                      get-header-names:func(max-len:u64, cursor:u32) ->result<tuple<string, option<u32>>, error>;
                      get-header-value:func(name:string, max-len:u64) ->result<option<list<u8>>, error>;
                      get-header-values:func(name:string, max-len:u64, cursor:u32) ->result<tuple<list<u8>, option<u32>>, error>;
                      set-header-values:func(name:string, values:list<u8>) ->result<_, error>;
                      get-method:func(max-len:u64) ->result<string, error>;
                      set-method:func(method:string) ->result<_, error>;
                      get-uri:func(max-len:u64) ->result<string, error>;
                      set-uri:func(uri:string) ->result<_, error>;
                      get-version:func() ->result<http-version, error>;
                      set-version:func(version:http-version) ->result<_, error>;
                      send:func(backend:borrow<backend>, body:body) ->result<response, error>;
                      }
                      }
                      interfacehttp-resp {
                      usetypes.{error};
                      usehttp-types.{http-version};
                      usehttp-body.{body};
                      resourceresponse {
                      new:static func() ->result<response, error>;
                      get-status:func() ->result<u16, error>;
                      set-status:func(status:u16) ->result<_, error>;
                      get-version:func() ->result<http-version, error>;
                      set-version:func(version:http-version) ->result<_, error>;
                      get-header-names:func(max-len:u64, cursor:u32) ->result<tuple<string, option<u32>>, error>;
                      get-header-value:func(name:string, max-len:u64) ->result<option<list<u8>>, error>;
                      get-header-values:func(name:string, max-len:u64, cursor:u32) ->result<tuple<list<u8>, option<u32>>, error>;
                      set-header-values:func(name:string, values:list<u8>) ->result<_, error>;
                      send-downstream:func(body:body, streaming:bool) ->result<_, error>;
                      }
                      }
                      interfacehttp-downstream {
                      usetypes.{ip-address, error};
                      usehttp-req.{request};
                      downstream-client-ip-addr:func(ds-request:borrow<request>) ->option<ip-address>;
                      downstream-server-ip-addr:func(ds-request:borrow<request>) ->option<ip-address>;
                      downstream-client-request-id:func(ds-request:borrow<request>, max-len:u64) ->result<string, error>;
                      downstream-tls-cipher-openssl-name:func(ds-request:borrow<request>, max-len:u64) ->result<option<list<u8>>, error>;
                      downstream-tls-protocol:func(ds-request:borrow<request>, max-len:u64) ->result<option<list<u8>>, error>;
                      downstream-tls-ja3-md5:func(ds-request:borrow<request>) ->result<option<list<u8>>, error>;
                      downstream-tls-ja4:func(ds-request:borrow<request>, max-len:u64) ->result<option<string>, error>;
                      }

                      WIT bindings: stubs/wit_world/imports/http_req.py, http_resp.py, http_downstream.py, http_body.py

                      API Design

                      Core types:

                      • Request - Wraps http_req.Request with Pythonic API (properties for method, uri, version)
                      • Response - Wraps http_resp.Response
                      • Headers - Dict-like interface for header manipulation
                      • Body - io.IOBase-compatible for streaming (use shutil.copyfileobj(), etc.)

                      Key features:

                      • Downstream metadata via request.downstream accessor:
                        • client_ip()IPv4Address | IPv6Address
                        • tls_cipher(), tls_ja3_md5(), tls_ja4() → TLS fingerprints
                        • compliance_region() → GDPR/data residency region
                      • Cache control: request.set_cache_override(ttl=..., surrogate_key=...)
                      • Backend requests: request.send(backend, body)Response
                      • Streaming: Bodies are file-like objects, work with stdlib

                      Example - Proxying with transformation:

                      defhandle(incoming_req, incoming_body):
                      # Access metadataclient_ip=incoming_req.downstream.client_ip()
                      # Transform requestincoming_req.headers['X-Forwarded-For'] =str(client_ip)
                      incoming_req.set_cache_override(ttl=3600, surrogate_key='user-data')
                      # Send to backend (streaming)response=incoming_req.send('origin', incoming_body)
                      returnresponse

                      Integration with Existing SDK

                      WSGI Adapter

                      Can wrap incoming WIT request in Request object and expose via environ['fastly.request']:

                      fromflaskimportFlask, request@app.route("/api/data")defget_data():
                      fastly_req=request.environ['fastly.request']
                      client_ip=fastly_req.downstream.client_ip()
                      return {"client_ip": str(client_ip)}

                      Requests Facade

                      The requests.get() / requests.post() API is for making outgoing requests (client use case). It should NOT be extended for proxying - that's what this low-level API is for.

                      Clear separation:

                      • Client pattern (outgoing): Use requests.get(url) - builds request from scratch
                      • Server/Proxy pattern (incoming): Use Request/Response API - transforms received request

                      The requests facade could use Request wrappers internally but public API stays the same.

                      Cross-SDK Comparison:

                      • Rust: Request/Response types wrapping HTTP standard types. Methods for headers, body streams, methods, URLs. Rich builder patterns. Strongly typed.
                      • Go: Standard *http.Request/*http.Response from stdlib with Fastly extensions via embedded fields/methods.
                      • JS: Standard Request/Response from Fetch API with Fastly extensions.

                      Recommended approach: Python should provide standard library-compatible types (similar to requests or urllib) while adding Fastly-specific extensions.

                      Viceroy Testing

                      Viceroy supports HTTP request/response handling with full metadata access in tests. The @on_viceroy decorator can provide synthetic requests with headers, bodies, and metadata.

                      Tests can verify:

                      • Request/response creation and manipulation
                      • Header handling (case-insensitive, multi-value)
                      • Body streaming and reading
                      • Downstream metadata access (may have defaults for TLS info in Viceroy)

                      HTTP operations are well-supported in Viceroy testing.

                      Reference

                      Activity

                      Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

                      Metadata

                      Metadata

                      Assignees

                      No one assigned

                        Labels

                        No labels
                        No labels

                        Type

                        No type

                        Projects

                        No projects

                          Milestone

                          No milestone

                          Relationships

                          None yet

                          Development

                          No branches or pull requests

                          Issue actions

                          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                          Skip to content

                          Add Lower Level (Complete) HTTP Request/Response API #56

                          Description

                          @posborne

                          Overview

                          Add low-level HTTP Request and Response wrappers for advanced use cases requiring direct control over HTTP primitives, streaming, and Fastly-specific features.

                          Context - What exists:

                          • WSGI adapter - Run Flask/Bottle apps unmodified
                          • requests facade - Client API for making backend calls (requests.get(), etc.)
                          • Low-level Request/Response API - This issue

                          What this enables:

                          • Streaming/proxying without buffering entire request/response bodies
                          • Access to Fastly-specific metadata (TLS fingerprints, client IP, compliance region)
                          • Request transformation (modify incoming request, send to backend)
                          • Cache control and surrogate key management
                          • Foundation for other SDK features (cache, security, image optimizer APIs)

                          When to use what:

                          Use CaseUse ThisNot This
                          Run Flask appWSGI adapterThis
                          Make API calls from apprequests.get()This
                          Proxy/stream requestsThis (Request/Response)requests facade
                          Need TLS/IP metadataThis (Request.downstream)WSGI
                          Cache override/surrogate keysThisrequests facade

                          WIT Interface

                          interfacehttp-req {
                          usetypes.{error};
                          usehttp-types.{http-version};
                          usehttp-resp.{response};
                          usehttp-body.{body};
                          usebackend.{backend};
                          resourcerequest {
                          new:static func() ->result<request, error>;
                          set-cache-override:func(cache-override:cache-override) ->result<_, error>;
                          get-header-names:func(max-len:u64, cursor:u32) ->result<tuple<string, option<u32>>, error>;
                          get-header-value:func(name:string, max-len:u64) ->result<option<list<u8>>, error>;
                          get-header-values:func(name:string, max-len:u64, cursor:u32) ->result<tuple<list<u8>, option<u32>>, error>;
                          set-header-values:func(name:string, values:list<u8>) ->result<_, error>;
                          get-method:func(max-len:u64) ->result<string, error>;
                          set-method:func(method:string) ->result<_, error>;
                          get-uri:func(max-len:u64) ->result<string, error>;
                          set-uri:func(uri:string) ->result<_, error>;
                          get-version:func() ->result<http-version, error>;
                          set-version:func(version:http-version) ->result<_, error>;
                          send:func(backend:borrow<backend>, body:body) ->result<response, error>;
                          }
                          }
                          interfacehttp-resp {
                          usetypes.{error};
                          usehttp-types.{http-version};
                          usehttp-body.{body};
                          resourceresponse {
                          new:static func() ->result<response, error>;
                          get-status:func() ->result<u16, error>;
                          set-status:func(status:u16) ->result<_, error>;
                          get-version:func() ->result<http-version, error>;
                          set-version:func(version:http-version) ->result<_, error>;
                          get-header-names:func(max-len:u64, cursor:u32) ->result<tuple<string, option<u32>>, error>;
                          get-header-value:func(name:string, max-len:u64) ->result<option<list<u8>>, error>;
                          get-header-values:func(name:string, max-len:u64, cursor:u32) ->result<tuple<list<u8>, option<u32>>, error>;
                          set-header-values:func(name:string, values:list<u8>) ->result<_, error>;
                          send-downstream:func(body:body, streaming:bool) ->result<_, error>;
                          }
                          }
                          interfacehttp-downstream {
                          usetypes.{ip-address, error};
                          usehttp-req.{request};
                          downstream-client-ip-addr:func(ds-request:borrow<request>) ->option<ip-address>;
                          downstream-server-ip-addr:func(ds-request:borrow<request>) ->option<ip-address>;
                          downstream-client-request-id:func(ds-request:borrow<request>, max-len:u64) ->result<string, error>;
                          downstream-tls-cipher-openssl-name:func(ds-request:borrow<request>, max-len:u64) ->result<option<list<u8>>, error>;
                          downstream-tls-protocol:func(ds-request:borrow<request>, max-len:u64) ->result<option<list<u8>>, error>;
                          downstream-tls-ja3-md5:func(ds-request:borrow<request>) ->result<option<list<u8>>, error>;
                          downstream-tls-ja4:func(ds-request:borrow<request>, max-len:u64) ->result<option<string>, error>;
                          }

                          WIT bindings: stubs/wit_world/imports/http_req.py, http_resp.py, http_downstream.py, http_body.py

                          API Design

                          Core types:

                          • Request - Wraps http_req.Request with Pythonic API (properties for method, uri, version)
                          • Response - Wraps http_resp.Response
                          • Headers - Dict-like interface for header manipulation
                          • Body - io.IOBase-compatible for streaming (use shutil.copyfileobj(), etc.)

                          Key features:

                          • Downstream metadata via request.downstream accessor:
                            • client_ip()IPv4Address | IPv6Address
                            • tls_cipher(), tls_ja3_md5(), tls_ja4() → TLS fingerprints
                            • compliance_region() → GDPR/data residency region
                          • Cache control: request.set_cache_override(ttl=..., surrogate_key=...)
                          • Backend requests: request.send(backend, body)Response
                          • Streaming: Bodies are file-like objects, work with stdlib

                          Example - Proxying with transformation:

                          defhandle(incoming_req, incoming_body):
                          # Access metadataclient_ip=incoming_req.downstream.client_ip()
                          # Transform requestincoming_req.headers['X-Forwarded-For'] =str(client_ip)
                          incoming_req.set_cache_override(ttl=3600, surrogate_key='user-data')
                          # Send to backend (streaming)response=incoming_req.send('origin', incoming_body)
                          returnresponse

                          Integration with Existing SDK

                          WSGI Adapter

                          Can wrap incoming WIT request in Request object and expose via environ['fastly.request']:

                          fromflaskimportFlask, request@app.route("/api/data")defget_data():
                          fastly_req=request.environ['fastly.request']
                          client_ip=fastly_req.downstream.client_ip()
                          return {"client_ip": str(client_ip)}

                          Requests Facade

                          The requests.get() / requests.post() API is for making outgoing requests (client use case). It should NOT be extended for proxying - that's what this low-level API is for.

                          Clear separation:

                          • Client pattern (outgoing): Use requests.get(url) - builds request from scratch
                          • Server/Proxy pattern (incoming): Use Request/Response API - transforms received request

                          The requests facade could use Request wrappers internally but public API stays the same.

                          Cross-SDK Comparison:

                          • Rust: Request/Response types wrapping HTTP standard types. Methods for headers, body streams, methods, URLs. Rich builder patterns. Strongly typed.
                          • Go: Standard *http.Request/*http.Response from stdlib with Fastly extensions via embedded fields/methods.
                          • JS: Standard Request/Response from Fetch API with Fastly extensions.

                          Recommended approach: Python should provide standard library-compatible types (similar to requests or urllib) while adding Fastly-specific extensions.

                          Viceroy Testing

                          Viceroy supports HTTP request/response handling with full metadata access in tests. The @on_viceroy decorator can provide synthetic requests with headers, bodies, and metadata.

                          Tests can verify:

                          • Request/response creation and manipulation
                          • Header handling (case-insensitive, multi-value)
                          • Body streaming and reading
                          • Downstream metadata access (may have defaults for TLS info in Viceroy)

                          HTTP operations are well-supported in Viceroy testing.

                          Reference

                          Activity

                          Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

                          Metadata

                          Metadata

                          Assignees

                          No one assigned

                            Labels

                            No labels
                            No labels

                            Type

                            No type

                            Projects

                            No projects

                              Milestone

                              No milestone

                              Relationships

                              None yet

                              Development

                              No branches or pull requests

                              Issue actions

                              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                              Skip to content

                              Add Lower Level (Complete) HTTP Request/Response API #56

                              Description

                              @posborne

                              Overview

                              Add low-level HTTP Request and Response wrappers for advanced use cases requiring direct control over HTTP primitives, streaming, and Fastly-specific features.

                              Context - What exists:

                              • WSGI adapter - Run Flask/Bottle apps unmodified
                              • requests facade - Client API for making backend calls (requests.get(), etc.)
                              • Low-level Request/Response API - This issue

                              What this enables:

                              • Streaming/proxying without buffering entire request/response bodies
                              • Access to Fastly-specific metadata (TLS fingerprints, client IP, compliance region)
                              • Request transformation (modify incoming request, send to backend)
                              • Cache control and surrogate key management
                              • Foundation for other SDK features (cache, security, image optimizer APIs)

                              When to use what:

                              Use CaseUse ThisNot This
                              Run Flask appWSGI adapterThis
                              Make API calls from apprequests.get()This
                              Proxy/stream requestsThis (Request/Response)requests facade
                              Need TLS/IP metadataThis (Request.downstream)WSGI
                              Cache override/surrogate keysThisrequests facade

                              WIT Interface

                              interfacehttp-req {
                              usetypes.{error};
                              usehttp-types.{http-version};
                              usehttp-resp.{response};
                              usehttp-body.{body};
                              usebackend.{backend};
                              resourcerequest {
                              new:static func() ->result<request, error>;
                              set-cache-override:func(cache-override:cache-override) ->result<_, error>;
                              get-header-names:func(max-len:u64, cursor:u32) ->result<tuple<string, option<u32>>, error>;
                              get-header-value:func(name:string, max-len:u64) ->result<option<list<u8>>, error>;
                              get-header-values:func(name:string, max-len:u64, cursor:u32) ->result<tuple<list<u8>, option<u32>>, error>;
                              set-header-values:func(name:string, values:list<u8>) ->result<_, error>;
                              get-method:func(max-len:u64) ->result<string, error>;
                              set-method:func(method:string) ->result<_, error>;
                              get-uri:func(max-len:u64) ->result<string, error>;
                              set-uri:func(uri:string) ->result<_, error>;
                              get-version:func() ->result<http-version, error>;
                              set-version:func(version:http-version) ->result<_, error>;
                              send:func(backend:borrow<backend>, body:body) ->result<response, error>;
                              }
                              }
                              interfacehttp-resp {
                              usetypes.{error};
                              usehttp-types.{http-version};
                              usehttp-body.{body};
                              resourceresponse {
                              new:static func() ->result<response, error>;
                              get-status:func() ->result<u16, error>;
                              set-status:func(status:u16) ->result<_, error>;
                              get-version:func() ->result<http-version, error>;
                              set-version:func(version:http-version) ->result<_, error>;
                              get-header-names:func(max-len:u64, cursor:u32) ->result<tuple<string, option<u32>>, error>;
                              get-header-value:func(name:string, max-len:u64) ->result<option<list<u8>>, error>;
                              get-header-values:func(name:string, max-len:u64, cursor:u32) ->result<tuple<list<u8>, option<u32>>, error>;
                              set-header-values:func(name:string, values:list<u8>) ->result<_, error>;
                              send-downstream:func(body:body, streaming:bool) ->result<_, error>;
                              }
                              }
                              interfacehttp-downstream {
                              usetypes.{ip-address, error};
                              usehttp-req.{request};
                              downstream-client-ip-addr:func(ds-request:borrow<request>) ->option<ip-address>;
                              downstream-server-ip-addr:func(ds-request:borrow<request>) ->option<ip-address>;
                              downstream-client-request-id:func(ds-request:borrow<request>, max-len:u64) ->result<string, error>;
                              downstream-tls-cipher-openssl-name:func(ds-request:borrow<request>, max-len:u64) ->result<option<list<u8>>, error>;
                              downstream-tls-protocol:func(ds-request:borrow<request>, max-len:u64) ->result<option<list<u8>>, error>;
                              downstream-tls-ja3-md5:func(ds-request:borrow<request>) ->result<option<list<u8>>, error>;
                              downstream-tls-ja4:func(ds-request:borrow<request>, max-len:u64) ->result<option<string>, error>;
                              }

                              WIT bindings: stubs/wit_world/imports/http_req.py, http_resp.py, http_downstream.py, http_body.py

                              API Design

                              Core types:

                              • Request - Wraps http_req.Request with Pythonic API (properties for method, uri, version)
                              • Response - Wraps http_resp.Response
                              • Headers - Dict-like interface for header manipulation
                              • Body - io.IOBase-compatible for streaming (use shutil.copyfileobj(), etc.)

                              Key features:

                              • Downstream metadata via request.downstream accessor:
                                • client_ip()IPv4Address | IPv6Address
                                • tls_cipher(), tls_ja3_md5(), tls_ja4() → TLS fingerprints
                                • compliance_region() → GDPR/data residency region
                              • Cache control: request.set_cache_override(ttl=..., surrogate_key=...)
                              • Backend requests: request.send(backend, body)Response
                              • Streaming: Bodies are file-like objects, work with stdlib

                              Example - Proxying with transformation:

                              defhandle(incoming_req, incoming_body):
                              # Access metadataclient_ip=incoming_req.downstream.client_ip()
                              # Transform requestincoming_req.headers['X-Forwarded-For'] =str(client_ip)
                              incoming_req.set_cache_override(ttl=3600, surrogate_key='user-data')
                              # Send to backend (streaming)response=incoming_req.send('origin', incoming_body)
                              returnresponse

                              Integration with Existing SDK

                              WSGI Adapter

                              Can wrap incoming WIT request in Request object and expose via environ['fastly.request']:

                              fromflaskimportFlask, request@app.route("/api/data")defget_data():
                              fastly_req=request.environ['fastly.request']
                              client_ip=fastly_req.downstream.client_ip()
                              return {"client_ip": str(client_ip)}

                              Requests Facade

                              The requests.get() / requests.post() API is for making outgoing requests (client use case). It should NOT be extended for proxying - that's what this low-level API is for.

                              Clear separation:

                              • Client pattern (outgoing): Use requests.get(url) - builds request from scratch
                              • Server/Proxy pattern (incoming): Use Request/Response API - transforms received request

                              The requests facade could use Request wrappers internally but public API stays the same.

                              Cross-SDK Comparison:

                              • Rust: Request/Response types wrapping HTTP standard types. Methods for headers, body streams, methods, URLs. Rich builder patterns. Strongly typed.
                              • Go: Standard *http.Request/*http.Response from stdlib with Fastly extensions via embedded fields/methods.
                              • JS: Standard Request/Response from Fetch API with Fastly extensions.

                              Recommended approach: Python should provide standard library-compatible types (similar to requests or urllib) while adding Fastly-specific extensions.

                              Viceroy Testing

                              Viceroy supports HTTP request/response handling with full metadata access in tests. The @on_viceroy decorator can provide synthetic requests with headers, bodies, and metadata.

                              Tests can verify:

                              • Request/response creation and manipulation
                              • Header handling (case-insensitive, multi-value)
                              • Body streaming and reading
                              • Downstream metadata access (may have defaults for TLS info in Viceroy)

                              HTTP operations are well-supported in Viceroy testing.

                              Reference

                              Activity

                              Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

                              Metadata

                              Metadata

                              Assignees

                              No one assigned

                                Labels

                                No labels
                                No labels

                                Type

                                No type

                                Projects

                                No projects

                                  Milestone

                                  No milestone

                                  Relationships

                                  None yet

                                  Development

                                  No branches or pull requests

                                  Issue actions