fix(x402): strip healthPath from buyer endpoint URL construction#539
Open
bussyjd wants to merge 1 commit into
Open
fix(x402): strip healthPath from buyer endpoint URL construction#539bussyjd wants to merge 1 commit into
bussyjd wants to merge 1 commit into
Conversation
`buy.py`'s `_normalize_endpoint` previously only trimmed trailing `/v1/chat/completions` and `/chat/completions` suffixes from the user-supplied `--endpoint`. When the seller's ServiceOffer declared a non-trivial `upstream.healthPath` (e.g. `/api/tags` for Ollama), and the endpoint URL passed to `buy.py buy` included that healthPath segment between `/services/<name>` and the LLM suffix, `_build_purchase_spec` then re-appended `/v1/chat/completions` on top of it. The resulting PurchaseRequest endpoint was `http://traefik.../services/demo-hello/api/tags/v1/chat/completions`, which 404s — `healthPath` is for the controller's upstream health probe only, never part of the publicly routed offer path (`/services/<name>`). Normalize the endpoint to its canonical offer base: when the path matches `/services/<segment>/...`, truncate to `/services/<segment>` before appending the LLM suffix. Preserves existing behaviour for non-`/services` URLs (external x402 sellers) and bare `/services/<name>` endpoints. Added `tests/test_buy_normalize_endpoint.py` covering the regression plus existing-suffix and non-`/services` URL cases.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
buy.py's_normalize_endpointonly stripped trailing/v1/chat/completionsand/chat/completionssuffixes. When a seller declared a non-trivialupstream.healthPath(e.g./api/tagsfor Ollama) and that segment leaked into the--endpointURL, the spec builder happily re-appended/v1/chat/completionson top, producing dead URLs likehttp://traefik.../services/demo-hello/api/tags/v1/chat/completions.Repro
Seller:
Buyer:
Before fix:
PurchaseRequest.spec.endpoint = http://traefik.../services/demo-hello/api/tags/v1/chat/completions. LiteLLMpaid/demoPOSTs through the x402-buyer sidecar to that URL → 404 from Traefik.Root cause
buy.py(skill layer). The seller-side surface was clean:internal/serviceoffercontroller/render.goandcontroller.goonly ever emitoffer.EffectivePath()(=/services/<name>) for storefront //skill.md/ registration /status.Endpoint.EffectiveHealthPath()is consumed exactly once, by the controller's upstream health probe atcontroller.go:589.internal/x402/serviceoffer_source.gosetsRouteRule.UpstreamURLtohttp://<svc>.<ns>.svc.cluster.local:<port>(no healthPath) andStripPrefixtooffer.EffectivePath().internal/x402/forwardauth.go::buildResourceURLechoes the incoming request URI back as the 402resource.url, but the seller never advertises a URL with healthPath inaccepts[].So the bug was confined to the buyer: it accepted any user-supplied URL containing
/services/<name>/<extra>...and didn't sanitise the<extra>tail before appending/v1/chat/completions.Fix
_normalize_endpointnow also detects/services/<segment>/...paths and truncates to/services/<segment>before appending the LLM suffix. External (non-/services) sellers are untouched.Test plan
tests/test_buy_normalize_endpoint.py(13 cases) covering:/v1/chat/completions//chat/completionsstrip behaviour preserved/services/<name>URL unchanged/api/tags,/health, nested paths, and/api/tags/v1/chat/completionsall collapse to/services/<name><normalized>/v1/chat/completionsmatches canonical buyer URL/servicesURLs left intact (external x402 sellers)go build ./...cleango test ./internal/x402/... ./internal/serviceoffercontroller/...greengo test ./...green except one pre-existing failure (TestWarnIfNoChatModel_EmitsWarnWhenNoModelsininternal/stack— unrelated, confirmed againstmain)tests/test_buy_autorefill.pystill pass (4 pre-existing socket failures from offline test env — confirmed againstmain)obol sell http demo-hello --health-path /api/tags ...thenbuy.py buy demo-hello --endpoint http://traefik.../services/demo-hello/api/tags ...and confirmpaid/demoreturns 200Coordination
Bundle PR #536 touches the same
buy.pyscript area. This is a small, surgical change inside_normalize_endpoint; conflict resolution post-merge of #536 should be a quick rebase.