Skip to content

Backend Requests Support via Requests-Compatible API Facade#27

Merged
posborne merged 14 commits into
mainfrom
posborne/backend-requests
Jan 13, 2026
Merged

Backend Requests Support via Requests-Compatible API Facade#27
posborne merged 14 commits into
mainfrom
posborne/backend-requests

Conversation

@posborne
Copy link
Copy Markdown
Member

These changes are a second pass with significant rework based on #13. I believe I addressed all the comments from that PR and then some with significantly more work put into adding additional tests, a rewrite of almost all of the code, introduction of snapshot testing, a port of the changes to use the new wit interfaces which model backends as a resource, etc.

- Add LocalTestServer for httpbin-like backend testing
- Enhance ViceroyTestBase with backend setup capabilities
- Add utility functions for response body reading
- Update build configuration and dependencies
- Improve temporary file handling with NamedTemporaryFile
- Add syrupy for snapshot testing.

This provides the foundation for backend testing and includes
utilities needed by the requests implementation.
- Implement fastly_compute.requests module with requests compatibility
- Support for both static and dynamic backends
- Advanced timeout configuration (connect, first_byte, between_bytes)
- WIT error mapping to map to requests-compat exceptions (there are
  some known failures here due to how viceroy behaves).
- requests-compatible response object with lazy loading
- Add backend-requests example application
- Comprehensive test suite with snapshot testing
@posborne posborne requested a review from erikrose December 23, 2025 22:51
Copy link
Copy Markdown
Member

@erikrose erikrose left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just the usual spate of small things, plus a thought on future-proof API design before we get too deeply entrenched. This will be a great façade to have included.

Comment thread examples/backend-requests.py Outdated
if not url:
return {"error": "url query parameter is required"}

backend = query.get("backend", None)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If query.get is anything like dict.get, the None is implied. But Bottle docs contradict themselves about whether the default is "" or None. :-(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll have to retest after the break as I can't recall; I would expect it to be None based on normal dict get behavior, though this isn't a straight up dict so explicit could be argued as favorable here (to potentially abuse the zen of python).

Comment thread examples/backend-requests.py Outdated
return {
"method": method,
"url": url,
"backend": kwargs.get("backend", None),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This None is implied.

Comment thread examples/backend-requests.py Outdated
demo: The demo/endpoint name for this error
"""
exc_print = StringIO()
traceback.print_exc(file=exc_print)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could use format_exc(), which directly returns a string, and save ourselves going in and out of a StringIO.

Comment thread fastly_compute/requests/__init__.py Outdated
Comment thread fastly_compute/requests/__init__.py Outdated
# Backend-specific features
response = requests.get(
"/api/endpoint",
backend="my-backend" # Use specific static backend
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we merge this with the same example under "Basic Usage"?

Comment thread fastly_compute/requests/response.py Outdated
Comment thread fastly_compute/wsgi.py
@classmethod
def teardown_class(cls):
"""Clean up test server."""
if hasattr(cls, "test_server"):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this is always true.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might have been a workaround for a type checker complaint, but I'll confirm.

Comment thread fastly_compute/test_server.py Outdated
Comment thread pyproject.toml
line-ending = "auto"

[tool.ruff.lint.pydocstyle]
convention = "google"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Less common, less tool support. Somewhere, maybe on the last review of this, I had a better comment about the tradeoffs.

Aha, ran across it while looking for something else.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this carried over from the base PR. For now, I would like to keep it to avoid massive additional churn on this PR. We can maybe shortly do a revisit to convention and tool-assisted rework of existing docs in a separate PR.

Use traceback.format_exc() and remove redundant None defaults from dict.get() calls
BREAKING CHANGE: backend → fastly_backend, timeout_config → fastly_timeout

Establishes clear naming convention for Fastly-specific parameters
Remove no-op assignment, simplify exception handling, and remove redundant checks
Remove redundant None from Any | None union
Reduce duplication in from_http_req_error() and from_wit_error()
Remove __init__ methods that only call super() with same signature
Allow exceptions to propagate for proper error handling and debugging
Make test_server variables private and remove unnecessary hasattr check
Previously we were doing a sanitize step that I do not believe
is really necessary.  We now use the lowered netloc directly
for the dynamic backend name.  Additionaly, a case related to
an empty netloc was removed as it is not possible to hit.
- Flip ternary to avoid 'not' (clearer logic)
- Remove redundant UnicodeError catch (subclass of ValueError)
- Improve resolve_backend() docstring with clearer algorithm explanation
Merge Fastly-specific features section into Basic Usage for better flow.
The examples now show a progression from simple to advanced usage.
Copy link
Copy Markdown
Member

@erikrose erikrose left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other than this one thing, I think we're ready to rock!

Comment thread fastly_compute/requests/__init__.py Outdated
# Add custom headers
for name, value in headers.items():
wit_request.insert_header(name, value.encode("utf-8"))
except ValueError as e:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we'd do ourselves a favor to tighten up the scope on such a generic error, instead putting this just around both calls to encode() if indeed that's all we expect to raise ValueError.

These changes remove some overly broad exceptions and restructure
the core request logic in a few ways:
- Remove insert a user-agent; I compared other SDKs and they do
  not  add this, so we shouldn't either.
- Don't override content-type if set explicitly.
- Simplified Host header handling; there was a workaround that
  is now patched in viceroy and I think not necessary.
- Reduced the sites where we perform hostcalls in request and
  made those sites be very specific with exception handling.
headers = headers if headers is not None else {}
if fastly_backend is not None:
host_header = headers.pop("Host", url_parsed.netloc)
wit_request.insert_header("Host", host_header.encode("utf-8"))
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a small note here, the commit message contradicts this slightly. I may revisit what we're doing here now that I landed a change for fastly/Viceroy#549, but for now I'm keeping this as-is for dynamic backends. I think we might not need to do it, but I also don't think it represents a problem either in this form.

Copy link
Copy Markdown
Member

@erikrose erikrose left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me!


# Add custom headers
for name, value in headers.items():
headers = headers if headers is not None else {}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You ought to be able to say headers or {} if you like, since {} is the only falsy value of a dict.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be fine; I wrote it this way to avoid extra allocations in the case of headers already being {} as an empty dict is falsy.

@posborne posborne merged commit 6d891ca into main Jan 13, 2026
3 checks passed
@posborne posborne deleted the posborne/backend-requests branch January 13, 2026 17:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants