fix: accept the Transform Platform API URL as server_url (0.46.2) - #352
Open
paulkarayan wants to merge 4 commits into
Open
fix: accept the Transform Platform API URL as server_url (0.46.2)#352paulkarayan wants to merge 4 commits into
paulkarayan wants to merge 4 commits into
Conversation
The Transform Platform's API Keys page, and the docs, hand you https://platform-api.transform.unstructured.io/api/v1. That works with curl and 404s every Platform call in this SDK: clean_server_url only stripped a path for unstructuredapp.io hosts, so the /api/v1 survived and the operation's own /api/v1/jobs/ was appended on top, producing /api/v1/api/v1/jobs/, which matches no route. Recognize unstructured.io hosts too, matched on domain boundaries rather than by substring, so a lookalike host such as unstructuredapp.io.example.com keeps its path and scheme. Clean the base URL in BaseSDK._get_url as well. An operation-level server_url= override bypasses the SDK-init hook, so client.jobs.list_jobs(request={}, server_url=...) was uncleaned and 404d the same way even after the domain fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The base URL is now cleaned in BaseSDK._get_url, which every operation passes through, so a self-hosted deployment behind a subpath is the regression this change could cause. The existing server-url cases mock _build_request and so cannot see it; assert on the request that is actually sent instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Formatting only, plus seven noqa directives for pre-existing lint that cannot be auto-fixed without changing behaviour. Two of them matter: `raise err` in basesdk re-raises the exception an after-error hook returned, which is not always the active one, so ruff's suggestion of a bare `raise` would be wrong. No behaviour change. pylint 10.00/10 and mypy stay clean, and the unit and contract suites pass on 3.11, 3.12 and 3.13. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Author
Repro-first proofBugfix — server_url copied from the Transform Platform API Keys page (.../api/v1) doubled the path and 404'd every Platform SDK call Reproduced the broken state
Failing test (red)
Fix
Proof it's resolved
Auto-generated from this branch's |
paulkarayan
marked this pull request as ready for review
August 22, 2026 18:14
There was a problem hiding this comment.
All reported issues were addressed across 7 files
Shadow auto-approve: would not auto-approve because issues were found.
Re-trigger cubic
…alified host Two problems cubic caught on the PR, both real. basesdk.py is generated and was not in .genignore, so a Speakeasy run would drop the _get_url cleaning and an operation-level server_url= would double its /api/v1 prefix again. Added the entry, following the same pattern as general.py, with a regeneration guard test to match the existing ones. Freezing the file also freezes the generated request plumbing, so the entry documents how to un-freeze it for a regen. is_unstructured_domain also missed a fully qualified host carrying the terminal root dot. That was a regression this branch introduced: the old substring test matched api.unstructuredapp.io. and stripped its path, the domain-boundary test did not. The path is stripped again; the host keeps the dot exactly as the caller wrote it, since it changes the Host header and SNI and is a deliberate choice. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
0 issues found across 4 files (changes from recent commits).
Shadow auto-approve: would not auto-approve. Auto-approval blocked by 1 unresolved issue from previous reviews.
Re-trigger cubic
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.
What & why
Problem: The Transform Platform's API Keys page hands you
https://platform-api.transform.unstructured.io/api/v1, and the docs tell you to pass that value asserver_url. Do it and every Platform call in this SDK fails with a 404: listing jobs, creating a workflow, checking a connector. The same URL works with curl, so the URL looks right and the SDK looks broken, and there is nothing in the error to point at the real cause. Anyone starting from the app's own copy button hits this on their first call.Change: Treat hosts under
unstructured.ioas Unstructured API hosts, so a copied/api/v1suffix is stripped fromserver_urlthe way it already was forunstructuredapp.io. Also clean the base URL for an operation-levelserver_url=override, which bypassed the cleaning hook entirely.Linked ticket
none
Client-facing follow-up: reported while writing the Transform Python quickstart, where every SDK sample had to be written against a URL different from the one the app displays.
The bug
Every Platform operation in this SDK already carries its own path prefix.
jobs.list_jobsrequests/api/v1/jobs/,workflows.create_workflowrequests/api/v1/workflows/, and so on. So the base URL must not carry/api/v1of its own.clean_server_urlexists to strip exactly that kind of pasted-in path, but it only did so when the host containedunstructuredapp.io:platform-api.transform.unstructured.iodoes not match, so the path was kept and the operation path was appended on top, giving/api/v1/api/v1/jobs/, which matches no route.basesdk.pyis generated, so the customization needs protecting: it is now in.genignore, the mechanism this repo already uses forgeneral.py,users.py,retries.pyandpartition.py, with a guard test alongside the existing ones asserting that both the_get_urlcall and the.genignoreentry survive. Without it a regeneration silently drops the fix and the doubled prefix returns. Freezing the file freezes the generated request, retry and hook plumbing too, so the entry carries the same un-freeze proceduregeneral.pydocuments.Three smaller problems came out of the same code while fixing it. The host test was a substring match, so
unstructuredapp.io.example.comwas treated as one of ours and had its path stripped and its scheme forced to HTTPS; it is now matched on domain boundaries and left alone. Aserver_url=passed to a single operation never reached the cleaning hook at all, because the hook runs at SDK init; that override is now cleaned inBaseSDK._get_url, the one point every operation's base URL passes through. And a fully qualified host carrying the terminal root dot (api.unstructuredapp.io.) has to be recognized explicitly, since the old substring test matched it by accident and the domain-boundary test does not; the path is stripped as before and the host keeps its dot, which changes the Host header and SNI and is the caller's choice to make.What the patch changes, and what it does not
Every
server_urlshape the existing tests, the docs and the app use, run throughclean_server_urlonmainand on this branch. Seven results change; sixteen are byte-identical.server_urlmainhttps://platform-api.transform.unstructured.io/api/v1https://platform-api.transform.unstructured.io/api/v1https://platform-api.transform.unstructured.iohttp://platform-api.transform.unstructured.io/api/v1http://platform-api.transform.unstructured.io/api/v1https://platform-api.transform.unstructured.ioplatform-api.transform.unstructured.io/api/v1http://platform-api.transform.unstructured.io/api/v1https://platform-api.transform.unstructured.ioplatform-api.transform.unstructured.iohttp://platform-api.transform.unstructured.iohttps://platform-api.transform.unstructured.iohttps://platform-api.unstructured.io/api/v1https://platform-api.unstructured.io/api/v1https://platform-api.unstructured.iohttp://unstructuredapp.io.example.com/api/v1https://unstructuredapp.io.example.comhttp://unstructuredapp.io.example.com/api/v1http://myunstructuredapp.io/api/v1https://myunstructuredapp.iohttp://myunstructuredapp.io/api/v1https://platform-api.transform.unstructured.iohttps://platform-api.transform.unstructured.iohttps://platform.unstructuredapp.io/api/v1https://platform.unstructuredapp.iohttps://api.unstructuredapp.io/general/v0/generalhttps://api.unstructuredapp.iounstructured-000mock.api.unstructuredapp.io/general/v0/generalhttps://unstructured-000mock.api.unstructuredapp.iohttp://localhost:8000http://localhost:8000localhost:8000http://localhost:8000http://localhost:8000/my/endpoint/http://localhost:8000/my/endpointlocalhost:8000/general/v0/generalhttp://localhost:8000/general/v0/generalhttps://unstructured.example.com/api/v1https://unstructured.example.com/api/v1http://not-unstructured.io/api/v1http://not-unstructured.io/api/v1The first five changed rows are the reported bug. The last two are the substring-match fix: those hosts are not ours, so they keep their path and their scheme.
Impact
Customers: Anyone using the Python SDK against the Transform Platform can now paste the API URL shown in the app, or set it from the documented
UNSTRUCTURED_API_URL, and have jobs, workflows, sources, destinations and templates calls work. Today that exact value 404s on every call. Users who already worked around it by passing the bare host are unaffected; that keeps working. Users onunstructuredapp.ioare unaffected; their URLs were already cleaned.Internal (devs / ops / other teams): The docs can stop steering readers away from the URL the product displays. No service imports this code; it is a client library published to PyPI.
Wire contract / clients: No request or response shape changes. The only behavior change is which URL a request is sent to, and only for base URLs that were previously producing a doubled path. The one case where a user could notice a difference is a self-hosted deployment on a host under
unstructuredapp.ioorunstructured.iothat genuinely serves the API beneath a subpath; that path is now stripped. Hosts outside those domains keep their path exactly as before, which the existing localhost subpath tests cover.Deployment target considerations: This is a PyPI client library, not a deployed service, so SaaS / DI / in-VPC / on-prem / SND deploys are unaffected. Air-gapped users pointing the SDK at their own hostname keep the existing keep-the-path behavior, since their host is not under an Unstructured domain.
A note on the diff size
The last commit is
ruff formatover the files this change touches, plus sevennoqadirectives for pre-existing lint that cannot be auto-fixed without changing behaviour. It is formatting only and carries no behaviour change, so reading the first two commits on their own gives you the whole fix. Two of thenoqas are worth knowing about:raise errinbasesdk.pyre-raises whatever an after-error hook returned, which is not always the active exception, so ruff's suggested bareraisewould be a real bug.Risk / rollback
Low. Small changes to URL normalization plus a
.genignoreentry, revert-safe, no migration and no flag.How it was verified
Ran the unit suite on Python 3.11, 3.12 and 3.13 and the contract suite, plus
pylint(10.00/10) andmypy, all green, matching what CI runs.uv.lockis unchanged, so theUV_LOCKED=1install holds. Reproduced the bug and then the fix against the live Transform Platform API without an API key, which is enough to tell the two apart: a route that exists answers 401, a route that does not answers 404. Not exercised with a real API key end to end, and not exercised against a self-hosted deployment.Proof
Repro, against the live API, before the fix:
Through the SDK, before the fix:
Failing tests at
HEADbefore the fix,_test_unstructured_client/unit/test_server_urls.py::test_platform_request_url_has_a_single_api_prefixplus the hook tests:After the fix, the same live check across both ways of passing the URL:
Every case now reaches the real route. Suites after the fix: unit and contract both pass,
pylint10.00/10,mypyclean.Dependencies / merge order
none
Worked Example
Release
Bumped to 0.46.2 with CHANGELOG and RELEASES entries.