Add UCP business profile proxy on a generalized proxy interceptor - #3976
Add UCP business profile proxy on a generalized proxy interceptor#3976malewis5 wants to merge 2 commits into
Conversation
eeddd11 to
fd2b5bb
Compare
| } | ||
| return null; | ||
| }, | ||
| mapError: (error) => ({ |
There was a problem hiding this comment.
mapError doesn't look at phase. So if setup fails, like a bad storeDomain making new URL throw, we return a 502 instead of the 500 default. I'd keep the factory's setup-500 / fetch-502 split here too.
| "cache-control": upstream.ok ? UCP_CACHE_CONTROL : UCP_NO_CACHE_CONTROL, | ||
| }), | ||
| }, | ||
| responseValidation: (upstream) => { |
There was a problem hiding this comment.
Right now an upstream 404 streams the Online Store's 404 HTML page straight to the agent. They asked for JSON at this path and get Shopify's branded error page instead. Maybe we should return a clean JSON 404 of our own?
| import { createProxyInterceptor } from "./proxy"; | ||
|
|
||
| const UCP_CACHE_CONTROL = | ||
| "public, max-age=60, s-maxage=60, stale-while-revalidate=300, stale-if-error=86400"; |
There was a problem hiding this comment.
The profile contains the store's signing keys. With stale-if-error=86400, an edge can keep serving rotated-out keys for up to a day if the origin has a bad time. I'd shorten this to 300.
| forwardSearch: false, | ||
| rewritePathname: () => UCP_PROFILE_PATH, | ||
| requestHeaders: { | ||
| allow: [], |
There was a problem hiding this comment.
We forward etag and last-modified but strip if-none-match, so clients always download the whole file again on revalidation. I'd allow if-none-match and if-modified-since through and pass an upstream 304 along. The 3xx check would need to skip 304 for that to work.
|
|
||
| export const handleUcpProxy = createProxyInterceptor({ | ||
| match: UCP_RE, | ||
| methods: ["GET"], |
There was a problem hiding this comment.
Only GET is handled, so a HEAD request falls through to the app's 404. HEAD is supposed to work wherever GET does, and checkout.ts already accepts both.
There was a problem hiding this comment.
🟡 Changes recommended
The UCP error mapper incorrectly converts internal setup failures into upstream 502 responses.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds UCP business-profile discovery to Hydrogen’s Shopify route handling using a generalized proxy interceptor.
Changes:
- Adds and registers
GET /.well-known/ucp. - Extends proxy configuration for header filtering, validation, caching, and error mapping.
- Adds tests, documentation, and a patch changeset.
File summaries
| File | Description |
|---|---|
.changeset/ucp-business-profile-proxy.md |
Records the patch release. |
packages/hydrogen/skills/hydrogen-request-handlers/SKILL.md |
Documents UCP integration and validation. |
packages/hydrogen/src/core/request-routing/handle-shopify-routes.test.ts |
Tests route-level UCP behavior. |
packages/hydrogen/src/core/request-routing/handle-shopify-routes.ts |
Registers the UCP interceptor. |
packages/hydrogen/src/core/request-routing/interceptors/proxy.test.ts |
Tests generalized proxy behavior. |
packages/hydrogen/src/core/request-routing/interceptors/proxy.ts |
Adds configurable proxy hooks. |
packages/hydrogen/src/core/request-routing/interceptors/ucp.test.ts |
Tests UCP proxy behavior and failures. |
packages/hydrogen/src/core/request-routing/interceptors/ucp.ts |
Implements the UCP profile proxy. |
packages/hydrogen/src/core/url.ts |
Adds the exact UCP route matcher. |
Review details
- Files reviewed: 9/9 changed files
- Comments generated: 1
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| mapError: (error) => ({ | ||
| status: error instanceof DOMException && error.name === "TimeoutError" ? 504 : 502, | ||
| headers: { "cache-control": UCP_NO_CACHE_CONTROL }, | ||
| }), |
WHY are these changes introduced?
Headless storefronts do not currently expose Shopify's managed UCP business profile on their public origin. Agents that begin discovery from a headless domain therefore cannot discover the authoritative UCP services and endpoints hosted on the store's
myshopify.comdomain.WHAT is this pull request doing?
This adds
GET /.well-known/ucptohandleShopifyRoutes, serving Shopify's managed UCP business profile from the headless storefront origin.Rather than a bespoke fetch implementation, the UCP route is now built on the existing
createProxyInterceptor, which was generalized to support the behavior UCP needs. This keeps a single proxy code path for all Shopify-owned routes.UCP route behavior
myshopify.comorigin withaccept: application/json, no shopper cookies, and no authorization./.well-known/ucpand does not forward the request query string.content-type,etag,last-modified,vary) and dropsSet-Cookieand internal upstream headers.Cache-Control: public, max-age=60, s-maxage=60, stale-while-revalidate=300, stale-if-error=86400to successful profiles, andCache-Control: no-storeto every error and unpublished profile.502.504and other network failures to502, both uncached.Proxy interceptor generalization
createProxyInterceptorgained opt-in hooks so a single implementation can back UCP and the existing proxies:forwardSearchto drop the query string from the upstream request.requestHeaders.applyStorefrontHeadersto skip storefront request-header propagation.responseHeaders.modeto switch between forwarding all upstream headers and an allowlist.responseHeaders.injectto set computed response headers (used for the UCP cache policy).responseHeaders.consumeStorefrontHeadersto skip request-context response-header consumption.responseValidationto reject an upstream response with a from-scratch body; the factory centrally drains the upstream body so no validator can leak the connection.mapErrorto set the error status and headers per phase, preserving the setup→500/ fetch→502default split.Documentation
hydrogen-request-handlersskill.Developer impact
Includes a patch changeset for
@shopify/hydrogen. This is an internal refactor plus a new Shopify-owned route; there is no new public API. Applications that already callhandleShopifyRoutesbefore framework routing serve the discovery profile automatically, and no migration is required.Risk
GET /.well-known/ucproute is intercepted. Other methods and paths continue to framework routing.createProxyInterceptornow backs the existing proxies as well, so its new response-validation and error-mapping paths are shared code. Behavior for the existing proxies is unchanged because the new hooks are opt-in.HOW to test your changes?
curl -i https://<storefront-origin>/.well-known/ucp.200, usesContent-Type: application/json, and includesCache-Control: public, max-age=60, s-maxage=60, stale-while-revalidate=300, stale-if-error=86400.myshopify.comorigin.curl -i -X POST https://<storefront-origin>/.well-known/ucpand confirm the request falls through to the application's normal routing.Post-merge steps
None.
Checklist