-
Notifications
You must be signed in to change notification settings - Fork 24
HTTP/2 #45
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
HTTP/2 #45
Conversation
335ee6a
to
6538a79
Compare
0b2e6cd
to
0b2fc0e
Compare
Possible future worK:
|
85989ab
to
8709a14
Compare
00f6c11
to
fa976ad
Compare
-- | ## `Aff` asynchronous API | ||
-- | | ||
-- | The __Node.HTTP2.Client.Aff__ and __Node.HTTP2.Server.Aff__ modules provide | ||
-- | a high-level asynchronous `Aff` API so that | ||
-- | [“you don’t even have to think about callbacks.”](https://github.com/purescript-contrib/purescript-aff/tree/main/docs#escaping-callback-hell) | ||
-- | That’s nice because when we attach callbacks to every possible event | ||
-- | and then handle the events, it’s hard to keep track of the order | ||
-- | in which events are occuring, and what context we’re in when an event | ||
-- | handler is called. | ||
-- | | ||
-- | With the `Aff` API we can write HTTP/2 clients and services in plain flat | ||
-- | one-thing-after-another | ||
-- | effect style. But network peers are perverse and they may not | ||
-- | send things to us in the order in which we expect. So we will want to | ||
-- | reintroduce some of the indeterminacy that we get from an event-callback | ||
-- | API. That’s what the | ||
-- | [`Parallel ParAff Aff`](https://pursuit.purescript.org/packages/purescript-aff/docs/Effect.Aff#t:ParAff) | ||
-- | instance is for. | ||
-- | We can use the functions in | ||
-- | [`Control.Parallel`](https://pursuit.purescript.org/packages/purescript-parallel/docs/Control.Parallel) | ||
-- | to run `Aff` effects concurrently. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is the place to write a small tutorial on Aff
. I think that is better served by people clicking on the Aff
type and reading any docs on that type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is the place to write a small tutorial on
Aff
.
Beginners are going to need this explanation. Maybe I should move this into the docs
directory?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've learned there is no docs
directory for any of the purescript-node repos. I’d rather decide whether to merge this PR and then move the docs around later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with @JordanMartinez that this isn't the right place for an Aff
tutorial, but it's a fine place to concretely demonstrate using this module via Aff
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Meaning, I think these docs should be moved to Aff
specifically, and then the module header above may say, "Refer to Aff
for more details. Here's a few examples of what you likely want." Aff
isn't well-documented in the first place aside from Nate's video on it and my learning repo which summarizes Nate's video in text/code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think these docs should be moved to
Aff
specifically, and then the module header above may say, "Refer toAff
for more details. Here's a few examples of what you likely want."
I love that idea but let's do it after this PR.
0ccbb5d
to
6184ea1
Compare
Question: Should the asynchronous functions be type |
I think the FFI should be using types like foreign import onStream :: Foreign -> (stream -> headers -> flags -> Effect Unit) -> Effect Unit export const onStream = foreign => callback => () => {
const cb = (stream, headers, flags) => callback(stream)(headers)(flags)();
foreign.on("stream", cb);
return () => {foreign.removeListener("stream", cb);};
}; we'd write foreign import onStreamImpl :: EffectFn2 Foreign (EffectFn3 stream headers flags Unit) (Effect Unit)
onStream :: Foreign -> (stream -> headers -> flags -> Effect Unit) -> Effect (Effect Unit)
onStream f cb = runFn2 onStreamImpl f (mkEffectFn3 cb) export const onStream = (foreign, callback) => {
const cb = (stream, headers, flags) => callback(stream)(headers)(flags)();
foreign.on("stream", callback);
return () => foreign.removeListener("stream", callback);
}; |
src/Node/HTTP2/Client.Aff.purs
Outdated
-- | | ||
-- | closeSession client | ||
-- | ``` | ||
module Node.HTTP2.Client.Aff |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The file path for this file should changed:
-src/Node/HTTP2/Client.Aff.purs
+src/Node/HTTP2/Client/Aff.purs
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really? I like this way. But if you insist, I'll make that change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just went to make that change and now I remember why I don't like this: because then we end up with two files named Aff.purs
. Which is not a good name for a file. And that convention gets worse as we scale up; if a repo has a lot of Effect
binding modules with a lot of Aff
API modules to go with them, then we just keep adding more files named Aff.purs
.
Is there some tooling reason why we would prefer to accumulate Aff.purs
files? Some tool which requires module prefixes to be subdirectories?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's nothing stopping someone from using qualified imports like
import Node.Http2.Client.Aff as Client.Aff
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really? I like this way. But if you insist, I'll make that change.
This just goes against convention with how everything else is done in core/contrib/web/node libs. I also wonder if this would break things in purs-backend-es
if we did go with this approach and someone defined a module using the convention and exposed a similarly named function defined here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok done.
Can you clarify why the |
ngHTTP2_String :: NGHTTP2 -> Maybe String | ||
ngHTTP2_String 0 = Just "NGHTTP2_NO_ERROR" | ||
ngHTTP2_String 1 = Just "NGHTTP2_PROTOCOL_ERROR" | ||
ngHTTP2_String 2 = Just "NGHTTP2_INTERNAL_ERROR" | ||
ngHTTP2_String 3 = Just "NGHTTP2_FLOW_CONTROL_ERROR" | ||
ngHTTP2_String 4 = Just "NGHTTP2_SETTINGS_TIMEOUT" | ||
ngHTTP2_String 5 = Just "NGHTTP2_STREAM_CLOSED" | ||
ngHTTP2_String 6 = Just "NGHTTP2_FRAME_SIZE_ERROR" | ||
ngHTTP2_String 7 = Just "NGHTTP2_REFUSED_STREAM" | ||
ngHTTP2_String 8 = Just "NGHTTP2_CANCEL" | ||
ngHTTP2_String 9 = Just "NGHTTP2_COMPRESSION_ERROR" | ||
ngHTTP2_String 10 = Just "NGHTTP2_CONNECT_ERROR" | ||
ngHTTP2_String 11 = Just "NGHTTP2_ENHANCE_YOUR_CALM" | ||
ngHTTP2_String 12 = Just "NGHTTP2_INADEQUATE_SECURITY" | ||
ngHTTP2_String 13 = Just "NGHTTP2_HTTP_1_1_REQUIRED" | ||
ngHTTP2_String _ = Nothing |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be an ADT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think so? Those integer codes come from deep inside Nghttp2 and I think that if we constrain them with the PureScript type system then we’re making type-level promises which we cannot actually guarantee.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good.
Yes.
Mostly because I think this way presents the nicest and simplest API to the user.
We could do it that way, that would be a reasonable approach. But in my opinion making that parameterized
than to present this little type substitution puzzle to the user and force them to mentally figure out that there are only two solutions.
And, furthermore, if the user already has Node.js experience then it's easy for them to recognize type names from Node.js. Because they are exactly the same.
So then after we've made this API choice, we're left with the question of how to implement it simply. I decided the simplest thing to do is to implement the FFI functions once and then P.S. I recently de-parameterized the int64 package in a similar way. I changed the API from |
Yeah that would be better. But it it doesn't change the API at all, so it could be done in a later PR with a patch version bump. And it won't make much difference to performance for this package, because the expensive part is the |
The worst problem with this PR is that you can't use this code to write a webserver that serves both HTTP/1 and HTTP/2 on the same port; see the PR description note. That seems to me like a catastrophic problem and solving it would require deleting everything and rewriting against a completely different Node.js HTTP/2 API. I didn't notice this was true until I had basically finished and was writing corner-case tests. |
I can agree with the above statement (that simpler types makes it easier for the developer) by disagreeing with the below comment:
I think we should minimize However, I'm still not sure this is worth doing. Other libraries (e.g. |
HTTP2 module with low-level `Effect` bindings and high-level `Aff` bindings. Add spago.dhall and spago.dev.dhall. Tests terminate. Use purescript-spec for tests. Upgrade ci.yml. New function HTTP.onRequest.
I agree with your characterization of the trade-offs here. I thought about it a bit and realized that we can get a lot more type-safety at the FFI by adding only a little bit more code, while keeping the same public API. I just added a commit which restricts the use of (I also found and fixed one type error while doing this) |
I think we disagree philosophically about how FFI should be written 😄 When I said "If there is a way to write the FFI without |
Regardless, I think we should ignore the FFI part of this PR for now. I should review the actual bindings themselves. I think I initially brought up the way the FFI was done because it made reviewing the bindings harder for me to follow. All in all, I just need to get more familiar with Node's
This may seem like a dumb question, but why is that bad? How often is |
Yeah 😄
Okay here's a thing we could do. We could use the PureScript type system to explictly declare which typecasts are legal. Apparently I'm not the first to consider this because this OCaml enthusiast has already implemented exactly what I had in mind: https://pursuit.purescript.org/packages/purescript-cast/0.1.0/docs/Data.Cast Then we can replace every remaining EDIT Or, I mean, we could do this without any type classes and just have a bunch of internal upcasting functions like: upcastClientHttp2Session :: ClientHttp2Session -> Http2Session
upcastClientHttp2Session = unsafeCoerce I'm going to do that and push another commit. EDIT I pushed a commit to type-annotate all |
I suppose it’s bad if one wants write a webserver for the public internet? I don't know how much public internet traffic is HTTP/1-only nowadays.
Maybe, I don't know. I would have to research and test that. |
.... I spent a bit of time looking through the Node docs and this PR. Emotionally, I'm.... irritated 😅. I was (probably unreasonably) expecting all the bindings from So, it's hard for me to review this PR for a few reasons:
|
I talked with some coworkers about this PR and want to propose how to deal with the current situation. At the end of the day, you have implemented some bindings. I'm sure they work because I know you're competent, and that if you came across some issue with them, you would fix it. Even though these bindings don't fully support the entirety of the Moreover, it was pointed out to me that the node libraries are designed to provide bindings specific to each node module. This makes it easier to know which library to use to get access to various bindings. When you asked whether these bindings should be in this library or a new one, I said to stick them here. This goes against the node library conventions/philosophy as to where things are stored. I was being undisciplined before. Now, I think the Anyway, here's what I'd like to propose. I propose we add a new repo for How does that sound? |
Fine. I'm ambivalent about this PR. When I started I was hoping that I would be able to understand the essential Node.js HTTP/2 API and that there would be one correct way to express it in Thanks for spending time reviewing this PR @JordanMartinez , you pointed out several areas for improvement in the future. |
(Note to myself or anyone else who wants to revive this: After I closed the Github PR I removed the third commit and force-pushed the http2 branch, which appears to have borked the Github PR. Anyway, the code I recommend using is the first two commits on the http2 branch.) |
@JordanMartinez Just to clarify, is the |
Yes, |
Excerpt from the docs for this PR. This is what the API basically looks like. Server-side exampleEquivalent to Node.js HTTP/2 Core API Server-side example import Node.Stream.Aff (write, end)
key <- Node.FS.Sync.readFile "localhost-privkey.pem"
cert <- Node.FS.Sync.readFile "localhost-cert.pem"
either (liftEffect <<< Console.errorShow) pure =<< attempt do
server <- createSecureServer (toOptions {key, cert})
listenSecure server (toOptions {port:8443})
\session headers stream -> do
respond stream
(toOptions {})
(toHeaders
{ "content-type": "text/html; charset=utf-8"
, ":status": 200
}
)
write (toDuplex stream) =<< fromStringUTF8 ("<h1>Hello World<hl>")
end (toDuplex stream) Client-side exampleEquivalent to Node.js HTTP/2 Core API Client-side example import Node.Stream.Aff (readAll)
ca <- liftEffect $ Node.FS.Sync.readFile "localhost-cert.pem"
either (liftEffect <<< Console.errorShow) pure =<< attempt do
client <- connect
(toOptions {ca})
(URL.parse "https://localhost:8443")
stream <- request client
(toOptions {})
(toHeaders {":path": "/"})
headers <- waitResponse stream
liftEffect $ for_ (headerKeys headers) \name ->
Console.log $
name <> ": " <> fromMaybe "" (headerString headers name)
body <- toStringUTF8 =<< (fst <$> readAll (toDuplex stream))
liftEffect $ Console.log $ "\n" <> body
closeSession client |
HTTP2
with low-levelEffect
bindings and high-levelAff
bindings. Solves Add support for http2 #44.ci.yml
. Solves Ensure that tests terminate #35.HTTP.onRequest
. Solves HTTP.createServer then close after one connection #46.spago.dhall
.The main new feature of this PR is HTTP/2 support. The main theme of this PR is “do all network I/O in the
Aff
monad.” The HTTP/2Effect
bindings are selected mostly because they are necessary to write theAff
API. Most of the documentation focuses on theAff
API.This PR is not a complete set of bindings to https://nodejs.org/docs/latest/api/http2.html , but it lays down what is in my opinion the correct foundation of vocabulary types and
Aff
idioms.If you want to understand what this PR is about, the best way to start is to clone the repo,
spago docs
, and read the module documentation for Node.HTTP2, Node.HTTP2.Server.Aff, and Node.HTTP2.Client.Aff.I expect the
newtype OptionsObject = OptionsObject Foreign
in Node.HTTP2 might be controversial. The Node.HTTP API is written in terms of Data.Options. Instead of writing the Node.HTTP2 API in terms of Data.Options, I treat the options object as aForeign
with a newtype wrapper. Users and downstream libraries can write their ownOption
types and useoptions
to construct theForeign
options object.I added a
spago.dhall
; that might also be controversial. None of the other purescript-node packages have aspago.dhall
. But spago is the only tool I know how to use, and also, with the imminent Registry change, all packages will soon have aspago.dhall
, right?UPDATE 2022-12-05
This PR does not support a listening server that can handle both HTTP/2 and HTTP/1.x. For that, apparently, we need to use a completely different Node.js API for the HTTP/2 server; the “Compatibility API”.
The Compatibility API requires us to listen for
'request'
events instead of'stream'
events, and then in the style of Node.jshttp
mutate aResponse
object which is passed to the'request'
handler.Checklist: