Skip to content

refactor(url): parse URLs as values instead of catching TypeErrors - #727

Merged
Makisuo merged 2 commits into
fix/05-github-enterprise-hostfrom
fix/06-url-parsing-as-values
Sep 1, 2026
Merged

refactor(url): parse URLs as values instead of catching TypeErrors#727
Makisuo merged 2 commits into
fix/05-github-enterprise-hostfrom
fix/06-url-parsing-as-values

Conversation

@Makisuo

@Makisuo Makisuo commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Sixth of the stack, based on #725.

new URL(...) throws a TypeError the type system cannot see, and every catch answering it flattens "not a URL" together with whatever else the block could fail on. Eight sites were spelling that out by hand, each with its own copy of the recovery.

@maple/domain/url holds the three shapes they wanted — parseUrl (Option), parseUrlWithBase for the relative form, and urlPathname, whose empty string is exactly what the request predicates in worker.ts were expressing as catch { return false }. The existing local copies in pull-request-ref and github-hosts use it too, so there is one implementation rather than four.

Converted: apps/api worker route predicates (×3), WorkersAiHttpClient, GithubProvider avatar derivation, PlanetScaleDiscoveryService, query-engine's hostOf, and url-validator.

Two behaviour changes fall out of the sweep

safeFetch could throw a raw TypeError out of the SSRF guard. It resolved a redirect's Location with a bare new URL(location, validated). That header is whatever the far end sent, so a malformed one escaped as neither a rejection callers can handle nor a response. It is a UrlValidationError now, with a regression test verified to fail against the old code.

validateExternalUrlSync threw UrlValidationError — a Schema.TaggedError, thrown, which validateExternalUrl then had to re-recognise with instanceof on the way back out. The check is now validateExternalUrlResult returning a Result, validateExternalUrl is Effect.fromResult over it, and both callers read the failure rather than catching it. All 69 existing guard cases are preserved as Result assertions — none dropped, none loosened.

safeFetch itself stays a Promise. Moving it onto HttpClient means rewriting the redirect and credential-stripping loop that #717 just landed, which is not a parsing change and wants its own PR.

Unrelated fix in the first commit

IngestAttributeMappingForbiddenError shipped with the admin gate (#713) but was never regenerated into ANTICIPATED_ERROR_IDENTIFIER_LIST, so the drift test comparing that list against reflection has been failing on main since it merged. The first commit is the regenerated output. Beyond the red suite, an ordinary 403 was being recorded as an error event rather than an anticipated 4xx.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

…pated

`IngestAttributeMappingForbiddenError` was added with the admin gate but never
regenerated into `ANTICIPATED_ERROR_IDENTIFIER_LIST`, so the drift test that
compares the checked-in list against reflection has been failing on main since.

Regenerated output only. Without it the error counts as a 5xx-style failure in
the SDK's span status, so an ordinary 403 would show up as an error event.
`new URL(...)` throws a `TypeError` the type system cannot see, and every
`catch` answering it flattens "not a URL" together with whatever else the block
could fail on. Eight sites were spelling that out by hand, each with its own
copy of the recovery.

`@maple/domain/url` holds the three shapes they wanted — `parseUrl` (Option),
`parseUrlWithBase` for the relative form, and `urlPathname`, whose empty string
is what the request predicates were expressing as `catch { return false }`. The
existing local copies in `pull-request-ref` and `github-hosts` now use it too.

Two behaviour changes fall out of the sweep:

- `safeFetch` resolved a redirect's `Location` with a bare
  `new URL(location, validated)`. That header is whatever the far end sent, so a
  malformed one threw a raw `TypeError` out of the SSRF guard — neither a
  rejection callers can handle nor a response. It is a `UrlValidationError` now,
  with a regression test.
- `validateExternalUrlSync` threw `UrlValidationError`, a `Schema.TaggedError`,
  which the Effect wrapper then had to re-recognise on the way back out. The
  check is `validateExternalUrlResult` returning a `Result`, `validateExternalUrl`
  is `Effect.fromResult` over it, and its two callers read the failure instead of
  catching it. All 69 existing guard cases are kept, as Result assertions.

`safeFetch` itself stays a Promise: moving it onto `HttpClient` is a rewrite of
the redirect and credential-stripping loop, not a parsing change.
An error occurred while trying to automatically change base from fix/05-github-enterprise-host to fix/04-self-hosted-ingestion-ui September 1, 2026 15:33
@Makisuo
Makisuo merged commit e47ce9c into fix/05-github-enterprise-host Sep 1, 2026
4 checks passed
@Makisuo
Makisuo deleted the fix/06-url-parsing-as-values branch September 1, 2026 17:16
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown

🍁 Maple PR preview

Warning

Preview cleanup could not be confirmed. The Alchemy teardown outcome was skipped.

Final commit a6ae916 · View workflow run

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.

1 participant