aiohttp icon indicating copy to clipboard operation
aiohttp copied to clipboard

aiohttp eroneous, incorrect, Client error "invalid constant string"

Open nhumrich opened this issue 7 years ago • 7 comments

Long story short

aiohttp is returning 400, message='invalid constant string' when nothing is actually wrong with the request or response.

Expected behaviour

I get the response object, which should be a 204

Actual behaviour

when I send a request, and get a 204 back, aiohttp throws this stack trace:

Traceback (most recent call last):
  File "/home/nhumrich/.virtualenvs/aviary/lib/python3.7/site-packages/aiohttp/client_reqrep.py", line 757, in start
    message, payload = await self._protocol.read()
  File "/home/nhumrich/.virtualenvs/aviary/lib/python3.7/site-packages/aiohttp/streams.py", line 543, in read
    await self._waiter
  File "/home/nhumrich/.virtualenvs/aviary/lib/python3.7/site-packages/aiohttp/client_proto.py", line 195, in data_received
    messages, upgraded, tail = self._parser.feed_data(data)
  File "aiohttp/_http_parser.pyx", line 523, in aiohttp._http_parser.HttpParser.feed_data
aiohttp.http_exceptions.BadHttpMessage: 400, message='invalid constant string'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/nhumrich/cnpy/aviary/aviary/service.py", line 53, in wrapped
    resp = await _function(session)
  File "/home/nhumrich/cnpy/aviary/aviary/canaries/billing.py", line 131, in create_invoice
    resp3 = await session.delete(f'/api/invoices/{new_invoice_id}')
  File "/home/nhumrich/cnpy/aviary/aviary/session.py", line 32, in delete
    return await self.send_request('DELETE', path, headers=headers, vars=vars)
  File "/home/nhumrich/cnpy/aviary/aviary/session.py", line 45, in send_request
    async with self._session.request(method, url, json=body, headers=headers) as resp:
  File "/home/nhumrich/.virtualenvs/aviary/lib/python3.7/site-packages/aiohttp/client.py", line 855, in __aenter__
    self._resp = await self._coro
  File "/home/nhumrich/.virtualenvs/aviary/lib/python3.7/site-packages/aiohttp/client.py", line 391, in _request
    await resp.start(conn)
  File "/home/nhumrich/.virtualenvs/aviary/lib/python3.7/site-packages/aiohttp/client_reqrep.py", line 762, in start
    message=exc.message, headers=exc.headers) from exc
aiohttp.client_exceptions.ClientResponseError: 400, message='invalid constant string'

I have tried to debug this as much as possible, but got stuck, and this is as far as I got. I noticed that in client_proto.py line 195 (as seen in the stack trace) that data is set to

b'HTTP/1.1 204 No Content\r\nDate: Thu, 07 Mar 2019 00:30:24 GMT\r\nContent-Length: 0\r\nConnection: keep-alive\r\nX-Via: heimdall\r\nServer: canopy-invoice\r\nAccess-Control-Allow-Origin: https://sub.example.domain.com\r\nAccess-Control-Allow-Credentials: true\r\nx-heimdall-target: rabbit\r\nX-Via: canopy-proxy-n\r\nX-Env: integ\r\nStrict-Transport-Security: max-age=31536000; includeSubDomains\r\n\r\nnull'

As you can see, there is a weird null at the end, even though content-length is 0. Wondering if this was a weird server bug, I tried to see what requests did and if I use requests-toolbelt to dump the raw response data, doing the exact same thing, the response has no null at the end:

bytearray(> HTTP/1.1 204 No Content\r\n> Date: Thu, 07 Mar 2019 00:00:27 GMT\r\n> Content-Length: 0\r\n> Connection: keep-alive\r\n> X-Via: heimdall\r\n> X-Via: canopy-proxy-n\r\n> Server: canopy-invoice\r\n> Access-Control-Allow-Origin: https://sub.example.com\r\n> Access-Control-Allow-Credentials: true\r\n> x-heimdall-target: rabbit\r\n> X-Env: integ\r\n> Strict-Transport-Security: max-age=31536000; includeSubDomains\r\n> \r\n')

If I call the same endpoint in the browser, there is no body shown. Unfortunately, this is a private endpoint that requires authentication, so I cant send a url to reproduce. But using that string about for data, you can reproduce the error. I have attempted to replace the data, and remove null, and if I remove the null at the end manually, (by editing aiohttp code locall, or using the debugger), everything works perfectly. The issue is that null is getting at the end somehow, and I am not sure how. I tried to put the debugger up higher in the stack, near read() in client_reqrep.py, but ended up getting very different errors, that seam to be race conditions with the headers when I attach a debugger.

Anyways, any thoughts as how the null is getting there?

This is actually a two-fold bug. First, the error makes it look like its a client error by sending a 400. I spent a significant amount of time thinking that the error was coming from the server because of the 400 status code. It would be great if this error made it more clear that the "server response" was invalid, and not my request.

But second, why is the null there, when no other client has it? Can this issue be solved?

Your environment

aiohttp==3.4.4 python==3.7.2 os==linux 4.20.5 using the client only.

any followup questions?

nhumrich avatar Mar 07 '19 00:03 nhumrich

GitMate.io thinks the contributor most likely able to help you is @asvetlov.

Possibly related issues are https://github.com/aio-libs/aiohttp/issues/3242 (Error), https://github.com/aio-libs/aiohttp/issues/1753 (aiohttp.errors is gone), https://github.com/aio-libs/aiohttp/issues/3251 (Aiohttp Client request limit), https://github.com/aio-libs/aiohttp/issues/2624 (aiohttp client throws http errors for the following redirect), and https://github.com/aio-libs/aiohttp/issues/2635 (Unclosed client session Error in aiohttp.request).

aio-libs-bot avatar Mar 07 '19 00:03 aio-libs-bot

@nhumrich can you explain what produces this response? Is it some server? Is it hard-coded? Is there perhaps a tcpdump / wireshark trace?

You assumption on the importance of the source of null is correct. Anyone who's going to work on it must know whence it comes.

For example, curl gives out a warning when there's extra data after Content-Length: 0 and http pipelining is off. curl however accepts the response and doesn't raise the exception.

requests swallows the unexpected body without exception. Perhaps it's a quirk introduced specifically to work around bad servers.

Chrome also swallows the unexpected body without error.

Thus, if heimdall proxy or the origin server actually sent the 4 bytes null, it could easily go unnoticed, as common tools handle such badness.

dimaqq avatar Mar 07 '19 10:03 dimaqq

@dimaqq I believe it's produced by http-parser on the client side.

webknjaz avatar Mar 07 '19 10:03 webknjaz

http-parser doesn't compile for me against Python-3.7.2, neither on linux nor mac.

dimaqq avatar Mar 07 '19 11:03 dimaqq

You probably don't have build deps then. Also, we ship OS-specific wheels so you don't have to compile anything during install time.

webknjaz avatar Mar 07 '19 12:03 webknjaz

For example, curl gives out a warning when there's extra data after Content-Length: 0

ok, good call, here is the result of curl:

  • Excess found in a non pipelined read: excess = 4 url = /api/invoices/149828 (zero-length body)

So, you might be right, the server probably is sending something. That helps. Thanks!

I would still like to keep this issue open, to change the error, and how it is presented to the user. Because 400 invalid constant string in no way represents the real error, which is a parsing error.

Also, perhaps, aiohttp could also act like most clients, and simply not read the body when the content-length is 0.

nhumrich avatar Mar 07 '19 18:03 nhumrich

Parsing etc. has changed substantially since this issue was opened. Can you retest if the problem still exists?

Dreamsorcerer avatar Aug 11 '24 23:08 Dreamsorcerer