Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transfer-Encoding:chunked tests #16678

Merged
merged 16 commits into from
Jan 14, 2021
Merged

Transfer-Encoding:chunked tests #16678

merged 16 commits into from
Jan 14, 2021

Conversation

vabresto
Copy link
Contributor

Follow up of PR #16636

This PR makes an important fix of excluding the newline separators from the final body, as this didn't come up in my use case and thus testing.

Additionally, it includes 4 tests:

Copy link
Contributor

@dom96 dom96 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! In general looks good, but see comments.

lib/pure/asynchttpserver.nim Outdated Show resolved Hide resolved
# Skip \r\n (chunk terminating bytes per spec)
when compileOption("assertions"):
let separator = await client.recv(2)
assert separator == "\r\n"
Copy link
Member

@timotheecour timotheecour Jan 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this fail in practice (eg client that violates spec)? if so, it's an external condition (not internal logic), so raising exception should be used instead

silently skipping the last 2 bytes can be really bad if they're not \r\n.

I doubt checking this condition has any real performance impact compared to rest of surrounding code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, it is external input, so sure, it can trigger the assert, but it would be a malformed request according to (my understanding of) the spec.

If the bytes are never received, the await should never finish, so I don't think an exception would be thrown.
If the bytes are received, but are not "\r\n", they simply get discarded. I also don't think this is exception worthy, especially if exceptions are explicitly disabled.

I'm not sure (and have no strong feelings) about what the "right" thing to do if the last two bytes are not "\r\n", but I would err towards saying that it is a malformed request (maybe return 400?) and is not a server exception.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do other languages do?

Copy link
Contributor Author

@vabresto vabresto Jan 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's what Python does: https://github.com/python/cpython/blob/63298930fb531ba2bb4f23bc3b915dbf1e17e9e1/Lib/http/client.py#L558

        if not chunk_left: # Can be 0 or None
            if chunk_left is not None:
                # We are at the end of chunk, discard chunk end
                self._safe_read(2)  # toss the CRLF at the end of the chunk

where self._safe_read is defined as

    def _safe_read(self, amt):
        """Read the number of bytes requested.
        This function should be used when <amt> bytes "should" be present for
        reading. If the bytes are truly not available (due to EOF), then the
        IncompleteRead exception can be used to detect the problem.
        """
        data = self.fp.read(amt)
        if len(data) < amt:
            raise IncompleteRead(data, amt-len(data))
        return data

Copy link
Member

@timotheecour timotheecour Jan 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok we can deal with it in future PR's then, keep the code as is (with when compileOption("assertions"): ...

maybe with a comment:

# xxx figure out whether to raise, assert or doAssert

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of adding a TODO just do it properly: raise a ValueError.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with ValueError and that was my initial wish too in #16678 (comment)


let port = Port(64123)
let server = newAsyncHttpServer()
discard server.serve(port, handler)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

discarding a future seems buggy (eg refs #11912)
/cc @dom96 do you have a suggestion that works here? (neither waitFor nor asyncCheck work here, giving RT errors)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vabresto in the meantime, add a comment:

# xxx figure out whether there's a better way than discarding a Future here

so we can move forward with this PR

@timotheecour
Copy link
Member

timotheecour commented Jan 12, 2021

followup to my recommendation in #16678 (comment), this works and is much better (not flaky):

diff --git a/lib/pure/asynchttpserver.nim b/lib/pure/asynchttpserver.nim
index ecbed8028..4d029c5c7 100644
--- a/lib/pure/asynchttpserver.nim
+++ b/lib/pure/asynchttpserver.nim
@@ -71,6 +71,9 @@ type
     maxBody: int ## The maximum content-length that will be read for the body.
     maxFDs: int

+proc getSocket*(a: AsyncHttpServer): AsyncSocket {.since: (1,5,1).} =
+  a.socket
+
 proc newAsyncHttpServer*(reuseAddr = true, reusePort = false,
                          maxBody = 8388608): AsyncHttpServer =
   ## Creates a new ``AsyncHttpServer`` instance.
diff --git a/tests/stdlib/tasynchttpserver_transferencoding.nim b/tests/stdlib/tasynchttpserver_transferencoding.nim
index 3c508902f..c384eae07 100644
--- a/tests/stdlib/tasynchttpserver_transferencoding.nim
+++ b/tests/stdlib/tasynchttpserver_transferencoding.nim
@@ -1,6 +1,9 @@
 import httpclient, asynchttpserver, asyncdispatch, asyncfutures
 import net

+import std/asyncnet
+import std/[nativesockets]
+
 const postBegin = """
 POST / HTTP/1.1
 Host: 127.0.0.1:64123
@@ -19,9 +22,11 @@ template genTest(input, expected) =
       sanity = true
       await request.respond(Http200, "Good")

-  let port = Port(64123)
   let server = newAsyncHttpServer()
-  discard server.serve(port, handler)
+  discard server.serve(Port(0), handler)
+  let port = getLocalAddr(server.getSocket.getFd, AF_INET)[1]
+
   let data = postBegin & input
   var socket = newSocket()
   socket.connect("127.0.0.1", port)

adding getSocket seems reasonable in this PR; in future PR we can make it easier to call let port = getLocalAddr(server.getSocket.getFd, AF_INET)[1]

This is a common use case, and not just for writing tests.

@vabresto
Copy link
Contributor Author

I will take a look and implement it tonight, but right away I notice that you have an echo (port,). Can I remove that, or what do I need to add to make the megatest happy about new output to stdout?

@timotheecour
Copy link
Member

timotheecour commented Jan 12, 2021

I notice that you have an echo (port,).

sorry, I just removed it (was just for debugging).

what do I need to add to make the megatest happy about new output to stdout?

not needed here, but in other situations, you'd use

discard """
  output: '''
blah blah
'''
"""

lib/pure/asynchttpserver.nim Outdated Show resolved Hide resolved
let chunk = await client.recv(bytesToRead)
request.body.add(chunk)
# Skip \r\n (chunk terminating bytes per spec)
when compileOption("assertions"):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need this, assert is already removed by the compiler when this option is not specified.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was told in a different comment (or maybe IRC) that doing something like this would save an extra string allocation. Additionally, if one compiles with asserts off, will

        let separator = await client.recv(2)
        assert separator == "\r\n"

cause an Unused identifier warning?

That said, I don't mind removing it as it definitely looks cleaner without it.

# Skip \r\n (chunk terminating bytes per spec)
when compileOption("assertions"):
let separator = await client.recv(2)
assert separator == "\r\n"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of adding a TODO just do it properly: raise a ValueError.

@timotheecour timotheecour mentioned this pull request Jan 12, 2021
await request.respond(Http200, "Good")

let server = newAsyncHttpServer()
discard server.serve(Port(0), handler)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(previous comment got lost)
discarding a future seems buggy (eg refs #11912)

note that #16695 doesn't have this issue (see its code with await server.serve(Port(5555), cb)), can you make something like it work? if not, add a comment:

# xxx figure out whether there's a better way than discarding a Future here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, I spent some time and dug through some docs, and with a bit of intuition and a lot of luck, I was able to replace this by using callSoon. I have no clue if that solution is any better than using discard, but I couldn't replicate what #16695 was doing.

Please advise.

Copy link
Member

@timotheecour timotheecour Jan 13, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EDIT: I don't know if callSoon is the right thing here; runSleepLoop seems more complicated than it should, there may be a better, but I don't know which, so please add a
# xxx check if there is a better way so we can move forward with this PR

Copy link
Contributor

@dom96 dom96 Jan 13, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's definitely not the right thing here. All you need to do is use asyncCheck on the code that calls asynchttpserver's serve. I think you've made the code harder to work with by creating that genTest template which is not necessary, turn it into an async proc and then waitFor that async proc.

@vabresto
Copy link
Contributor Author

For some reason I can't reply to #16678 (comment) but I'm confused:

Instead of adding a TODO just do it properly: raise a ValueError.

I'm not sure what TODO you're talking about, but more importantly, I don't believe this should raise a ValueError because I don't want my server to crash if a client sends a bad request. I believe this should be handled by sending back an HTTP 400 (or similar) response, and having the client retry.

@timotheecour
Copy link
Member

I don't believe this should raise a ValueError because I don't want my server to crash if a client sends a bad request. I believe this should be handled by sending back an HTTP 400 (or similar) response, and having the client retry.

seems fine; if needed followup work can allow customizing this

Copy link
Member

@timotheecour timotheecour left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM after addressing final small nits

@dom96
Copy link
Contributor

dom96 commented Jan 13, 2021

I'm not sure what TODO you're talking about, but more importantly, I don't believe this should raise a ValueError because I don't want my server to crash if a client sends a bad request. I believe this should be handled by sending back an HTTP 400 (or similar) response, and having the client retry.

Yeah, you're right, for some reason I thought I was looking at httpclient code. Too much context switching :)

@timotheecour
Copy link
Member

for whoever's reading this, the only remaining point is #16678 (comment)

@vabresto
Copy link
Contributor Author

Sadly, I don't know enough about Nim's async to resolve #16678 (comment) myself.

I've tried the following:
https://pastebin.com/QhdNgKbR
https://pastebin.com/55enx11T

but both error out with

Exception message: Bad file descriptor
Exception type: [OSError]

I also don't feel good about using discard now that I know it also discards any exceptions from the future (https://nim-lang.org/docs/asyncdispatch.html#discarding-futures). I'm hesitant to suggest using the variant with callSoon because while it works, and although I think I know why it works, I'm not sure. Please let me know how we should proceed.

@Araq
Copy link
Member

Araq commented Jan 14, 2021

Please let me know how we should proceed.

Ping @dom96, @timotheecour

@timotheecour
Copy link
Member

does rebasing against devel help now that #16436 was fixed? note that the corresponding PR #16695 used a similar construct (with await, not discard) and it worked, maybe because of its bugfix?

@vabresto
Copy link
Contributor Author

No, it doesn't unfortunately. I rebased locally and tried both of the snippets linked above and both still had

Exception message: Bad file descriptor
Exception type: [OSError]

However, the variant with callSoon still works (I tried intentionally breaking one of the tests, and it failed as expected).

Here's why I think that works:

  1. We call waitFor runSleepLoop, which blocks until that function completes
  2. runSleepLoop calls asyncdispatch.callSoon wrapper, which wraps waitFor server.acceptRequest(handler)
  3. This means that when waitFor runSleepLoop completes, the dispatcher has waitFor server.acceptRequest(handler) in it
  4. However, as soon as runSleepLoop completes, control returns to the main thread, which now executes the socket setup code synchronously.
  5. After the socket sends its data, we reach waitFor sleepAsync(10), which sleeps the main thread and returns control to the dispatcher, which now runs waitFor server.acceptRequest(handler)
  6. The server is able to now process the request and run the handler

There was a question earlier about why the server is able to process this request if it is sleeping when the request arrives; my explanation is that we do server.listen(Port(0)) in runSleepLoop, so the port is connected and the data has somewhere to (temporarily) live. I assume that there is some OS buffering of the port which is why this works.

I've also tried this snippet https://pastebin.com/wJwXpME3 after rebasing, per #16678 (comment) but this just stops at the serve (ie. the server is serving, but none of the rest of the code gets executed).

As far as my understanding goes, the code currently in this PR is the "best" variant. If there's anything else I should try, please let me know.

@Araq Araq merged commit a90f7a6 into nim-lang:devel Jan 14, 2021
@Araq
Copy link
Member

Araq commented Jan 14, 2021

We can always improve it further in follow-up PRs.

@dom96
Copy link
Contributor

dom96 commented Jan 14, 2021

For reference, I explained how to resolve this here: https://irclogs.nim-lang.org/13-01-2021.html#23:38:06

@vabresto
Copy link
Contributor Author

vabresto commented Jan 14, 2021

@dom96 Yes, I saw that (and thanks for your help in IRC, I wouldn't have been able to get this far without help from you and the community), but I wasn't able to get it working, it had the exact same errors listed above. If you've gotten it to compile and run without errors, can you please make a PR? I'm still very new to Nim so I'd love to see what it was that I'm missing. Plus, if what I've done here isn't best practices, it would be great to have a correct implementation for people to refer to in the future.

@timotheecour
Copy link
Member

@vabresto this test seems flaky, see timotheecour#544

not sure whether it relates to discarded future.

@vabresto
Copy link
Contributor Author

vabresto commented Jan 21, 2021

@timotheecour A few notes based on briefly looking around:

  • I noticed your branch is ~150 commits behind nim's devel, that may or may not matter.
  • The error is about sanity genSym 0. I'm not sure what genSym is (I'm guessing something like generate symbol based on context) but sanity is the boolean variable used to make sure the test runs.
  • I noticed your theory on the wait not being long enough, that would be interesting to test. If you increase the wait, does the flakiness reduce?

Can you try to update your branch? I would've expected that a lot more PR runs would've failed if this was being flaky. Other suggestion would be to try getting rid of the template (just copy-paste the code) and see if that works better, but note that I've run into issues before where running the code inside the template just once would be fine, but twice (like in a copy-paste) would fail.

Other question is how flaky is it? Have you run into this before?

@timotheecour
Copy link
Member

timotheecour commented Jan 21, 2021

I noticed your branch is ~150 commits behind nim's devel, that may or may not matter.

that's unrelated; the CI logs shown was for a PR against latest devel (2b5841c)

The error is about sanity genSym 0. I'm not sure what genSym is (I'm guessing something like generate symbol based on context) but sanity is the boolean variable used to make sure the test runs.

sanity is set to true when handler is run; so this shows that handler doesnt' run all the time

Can you try to update your branch?

not relevant as mentioned earlier

If you increase the wait, does the flakiness reduce?

haven't tried

I would've expected that a lot more PR runs would've failed if this was being flaky.

the test may fail on some low percentage of cases, but it's still a bug (a low percentage could translate in a high percentage depending on use cases). Since this test is joined in testament, it can also be that this bug triggers or not depending on unrelated conditions; one such example was the bug fixed in this PR #14327 but in your case the bug is likely different.

Other suggestion would be to try getting rid of the template (just copy-paste the code) and see if that works better,

this would just obfuscate / work around the underlying bug; we should expose and find the bug, not hide it ;-)

One way would be to run the test N times in a test PR (and still making it run inside megatest which seems to cause the issue); once there's a reliable way to reproduce the bug, it's easier to identify / fix.

Other question is how flaky is it? Have you run into this before?

1st time I see it, it may not be very flaky but it's still a bug that can impact user code if they use the same pattern

@dom96
Copy link
Contributor

dom96 commented Jan 27, 2021

Using callSoon here is very likely just a workaround and you are just hiding a crash, so I wouldn't be surprised if this is flaky. In fact, I wonder, have you checked that your test works? As in, it fails when your changes are reverted? Because if that doesn't happen then this test isn't useful.

@vabresto
Copy link
Contributor Author

I don't know if it is or isn't hiding a crash, just that I ran it a bunch of times and I didn't have any issues - like I said above, I don't know enough about Nim (and especially Nim async). I just did my best, and am happy to get feedback. That said, I did test that the test works and is correct. I tried changing the text that gets matched, and if I do change it, the test fails. This suggests to me that it is in fact a useful test. And for what it's worth, I was testing on OSX 10.15.6, so I don't know if it's particularly flaky on OSX.

ardek66 pushed a commit to ardek66/Nim that referenced this pull request Mar 26, 2021
* Add tests and fix extra newlines in body

* Fixes per comments

* Slight rephrase per comments

* Improvements per comments

* Add getSocket to reduce test flakiness per comment

* Remove unused lines from header

* Add doc comment to getSocket per comment

* Apply witchcraft to replace `discard Future`

* Return HTTP 400 on bad encoding in request

* Fix runnable example for getSocket

* Fix import to fix runnable examples

* Even more imports for the example

* Better self documenting runnable example

* Add missing import

* Import from module with correct signature

* Resolve port type mismatch
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants