Fix malformed HTTP responses from /capabilities and /availability (NEA hotfix branch) - #26
Conversation
Both VOSI endpoints emitted their XML document with no HTTP status line and no headers at all -- the first bytes on the wire were `<?xml version="1.0"...`. These are nph- (non-parsed-headers) CGI endpoints, so the script owns the entire response; nginx and Cloudflare reject the result as a bad upstream response. Every other response in tap.py already prints a CRLF-terminated status line and headers; these two were simply missing them. Emit `HTTP/1.1 200 OK`, `Content-type: text/xml` and the blank header-terminating line, all CRLF-terminated, matching the existing convention in this file (print supplies the LF after the literal \r). Also dispatch these two endpoints before workspace resolution. They are static documents that need no workspace, but they were falling through to the "retrieve workspace from jobid" branch with an empty jobid. That resolved to <workdir>/TAP and returned a 500 whenever that directory did not already exist -- so on a fresh deployment these endpoints failed outright, and on an established one they reached the emitter and produced the header-less response above. Verified by driving the CGI directly for both endpoints: the response now begins with a CRLF-terminated 200 status line, and works whether or not <workdir>/TAP exists. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
jpl-jengelke
left a comment
There was a problem hiding this comment.
The only thing I prefer is a different format for printing the header lines.
print('HTTP/1.1 200 OK', end='\r\n')
print('Content-Type: text/plain', end='\r\n')
print('', end='\r\n')
Why? It flushes immediately and makes clear the intent. We had a lot of problems with CRLF requirements as proxies are very particular.
|
Fantastic! Spot on, and I understand nea is behind main so not an issue, I can't merge but I'll quickly pull the revision branch and provide feedback shortly |
Review preference from @jpl-jengelke: make the CRLF requirement explicit at each print rather than relying on a trailing \r plus print's implicit \n. Byte output is unchanged -- both forms emit `...\r\n` -- but the intent is legible without having to remember what print appends, which matters on a file where a bare LF is what broke the endpoint. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review preference from @jpl-jengelke on #26, mirrored here to keep the two branches in sync. Make the CRLF requirement explicit at each print rather than relying on a trailing \r plus print's implicit \n. Byte output is unchanged -- both forms emit `...\r\n`, and the wire-level tests in tests/test_vosi_http_response.py still pass unmodified -- but the intent is legible without having to remember what print appends, which matters on a file where a bare LF is what broke the endpoint. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Adopted on both branches — print ('HTTP/1.1 200 OK', end='\r\n')
print ('Content-type: text/xml', end='\r\n')
print ('', end='\r\n')The readability argument is the one that sold it. Both forms emit byte-identical One correction on the rationale, since it could bite elsewhere: $ python -c "print('A', end='\r\n'); import os; os.write(1, b'B')"
BAThe raw It happens not to matter for these two endpoints: both print everything and then Kept |
Live test-server results:
|
| Endpoint | Result |
|---|---|
/availability |
407 bytes, well-formed XML. Fixed. |
/tables |
1,565,567 bytes, well-formed XML. Fine. |
/capabilities |
HTTP 200 with correct CRLF headers and a valid capabilities document — followed by a second complete HTTP response embedded in the first one's body. |
The seam in the /capabilities body:
90 </vosi:capabilities>
91 \r
92 HTTP/1.1 400 ERROR\r
93 Content-type: application/xml\r
94 \r
95 <?xml version="1.0" encoding="UTF-8"?>
96 <VOTABLE version="1.4" xmlns="http://www.ivoa.net/xml/VOTable/v1.3">
97 <RESOURCE type="results">
98 <INFO name="QUERY_STATUS" value="ERROR">
99 Input 'query' is blank.
That trailing block is what produces the browser's "Extra content at the end of the document". The HTTP framing is fine; the body is polluted.
Root cause
__printVosiCapability__ emits its XML and then returns instead of terminating. Execution falls through into the normal query-handling path, which finds no QUERY parameter and emits its own complete 400 response into the already-open 200.
Confirmed by taking 043192b from this branch, replacing the sys.exit() at the end of __printVosiCapability__ with pass, and running it against a local fixture:
simulated (this branch minus sys.exit) live server
93 </vosi:capabilities> 90 </vosi:capabilities>
91 \r
94 HTTP/1.1 400 ERROR\r 92 HTTP/1.1 400 ERROR\r
95 Content-type: text/xml\r 93 Content-type: application/xml\r
96 \r 94 \r
97 <?xml version="1.0" ... 95 <?xml version="1.0" ...
Same failure, byte for byte.
The deployed build is not this branch
Two tells in the live bytes:
- Live emits
Content-type: application/xml. This branch emitstext/xml, in both VOSI emitters and in__printError__. - Live has a stray
print("\r")after</vosi:capabilities>that this branch does not have.
So NEA is running a different patch. That one fixed the headers on both endpoints and fixed availability's exit, but dropped the sys.exit() at the end of the capabilities emitter.
This branch does not have the bug. Running 043192b directly against a pristine TAP_WORKDIR:
/availability 405 bytes HTTP/1.1 200 OK\r\n well-formed XML
/capabilities 3547 bytes HTTP/1.1 200 OK\r\n well-formed XML
Both terminate cleanly, neither creates a workspace. Same result on the develop-based branch in #27, which is covered by the regression tests there.
Fix is either one line — restore sys.exit() at the end of __printVosiCapability__ in the deployed variant — or deploy this branch, which already has it.
Cloudflare is not involved: cf-cache-status: DYNAMIC on both endpoints, so nothing stale is being served.
Unrelated issue, but worth flagging
The public capabilities document advertises internal URLs:
<accessURL use="full">http://vmexoweb2.ipac.caltech.edu:8080/TAP</accessURL>
<accessURL use="full">http://vmexoweb2.ipac.caltech.edu:8080/TAP/tables</accessURL>
<accessURL use="full">http://vmexoweb2.ipac.caltech.edu:8080/TAP/capabilities</accessURL>
<accessURL use="full">http://vmexoweb2.ipac.caltech.edu:8080/TAP/availability</accessURL>Any IVOA client that reads /capabilities for endpoint discovery (TOPCAT, pyvo) will be sent to that internal host and port rather than https://exoplanetarchive.nexsci.services. That is HTTP_URL / HTTP_PORT in the deployment's TAP.conf, not a code issue — but it will start mattering the moment /capabilities parses again.
Correction: this branch is fine — the live server is running a stale buildMy previous comment concluded that the capabilities emitter had lost its
One response per request, no fall-through, no trailing 400. The branch as it stands is correct. What So the diagnosis stands but the conclusion flips. The fall-through on Worth confirming after redeploy: Should print Non-blocking follow-up
<outputFormat ivo-id="ivo://ivoa.net/std/TAPRegExt#output-votable-td">
<mime>application/x-votable+xml</mime>
<alias>votable</alias>
</outputFormat>
Also unchanged from my last commentThe public capabilities document still advertises |
Problem
/capabilitiesand/availabilityemit their XML document with no HTTP status line and no headers at all — the first bytes on the wire are<?xml version="1.0"....These are nph- (non-parsed-headers) CGI endpoints, so the script owns the entire response. nginx and Cloudflare reject the result as a bad upstream response, which is what's breaking these two endpoints in the RedHat/NGINX deployment. Both are IVOA requirements.
This is not a CRLF-variant problem. Every other hand-rolled response in the codebase is already correct —
tap.py(×11) andvositables.py(×1) all useprint("...\r"), which yields a proper\r\n. These two endpoints were simply missing their headers entirely, which is why standard TAP requests were unaffected.Second defect
Both endpoints fell through to the "retrieve workspace from jobid" branch with an empty jobid. That resolved to
<workdir>/TAPand returned a 500 whenever that directory did not already exist.So on an established server the directory exists, the request reaches the emitter, and you get the header-less response above. On a fresh deployment these endpoints fail outright. They're static documents that need no workspace at all.
Changes
HTTP/1.1 200 OK,Content-type: text/xml, and the blank header-terminating line — all CRLF-terminated, matching the existing convention in this file (printsupplies the LF after the literal\r).Verification
Driven directly against the CGI for both endpoints:
Before the patch the same command returned
<?xml...as the very first line. Confirmed working both with and without<workdir>/TAPpresent. Existing suite: 10 passed.Note on scope
This branch is 81 commits behind
mainand predates the HTTP test fixture, so it carries the code fix only. The companion PR againstdevelopincludes wire-level regression tests.🤖 Generated with Claude Code