-
Notifications
You must be signed in to change notification settings - Fork 634
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
Redesign of http server module #188
Conversation
http/server.ts
Outdated
resolve: () => void; | ||
reject: () => void; | ||
} | ||
export type HttpHandler = (req: ServerRequest) => Promise<any>; |
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 Promise<any>
return type is bad, since it's not type safe. Can't it be Promise<void>
since we don't expect any return value?
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 know. but :Promise<void>
function can't accept one-line arrow function with non-void return vlaue:
server.handle("/", req => notVoidFunction(req))
I don't know which is better. But I chose one not restrictive.
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.
How about this?
export type HttpHandler = (req: ServerRequest) => Promise<any>; | |
export type HttpHandler = (req: ServerRequest) => void | Promise<void>; |
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.
what about Promise<unknown>
?
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.
Or just
export type HttpHandler = (req: ServerRequest) => Promise<any>; | |
export type HttpHandler = (req: ServerRequest) => unknown; |
since TS should let you await
something that’s not a promise.
http/server.ts
Outdated
function serveConn(env: ServeEnv, conn: Conn, bufr?: BufReader) { | ||
readRequest(conn, bufr).then(maybeHandleReq.bind(null, env, conn)); | ||
} | ||
export type ServerResponder = (response: ServerResponse) => Promise<any>; |
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.
Same as in HttpHandler
http/server.ts
Outdated
} | ||
yield readRequest(raced, async response => { | ||
await writeResponse(raced, response); |
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'm pretty sure we'll need access to ServerRequest
during response, this need already arose in #186
http/server.ts
Outdated
status?: number; | ||
headers?: Headers; | ||
body?: Uint8Array | Reader; | ||
export type HttpServerHandler = HttpHandler; |
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.
this type is not used anywhere and it's just an alias, is it needed?
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.
No, just forgotten to delete
http/server.ts
Outdated
public async body(): Promise<Uint8Array> { | ||
return readAllIterator(this.bodyStream()); | ||
} | ||
class ServerRequestInternal { |
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.
Same as for HttpServerHandler
http/server.ts
Outdated
async respond(r: Response): Promise<void> { | ||
return writeResponse(this.w, r); | ||
} | ||
function isServerRequest(x): x is ServerRequest { |
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.
Not used anywhere
http/server.ts
Outdated
}; | ||
} = Object.create(null); | ||
|
||
handle(pattern: string, handler: HttpHandler) { |
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'm not sure if adding routing here is a good thing, but nevertheless I'd suggest addHandler
name for this function.
Actually seen example on Twitter and I like it very much
http/server.ts
Outdated
// Continue read more from conn when user is done with the current req | ||
// Moving this here makes it easier to manage | ||
serveConn(env, result.conn, result.r); | ||
const raced = await Promise.race<Conn | void>([ |
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.
Can you add a comment explaining this bit?
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 remember that Promise.race
has a chance of starving? As it always scans from left to right
reqQueue: ServerRequest[]; | ||
serveDeferred: Deferred; | ||
} | ||
export type ServerRequest = { |
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.
Out of curiosity, what's the advantage of using interface over class?
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.
For test mocking. In test codes, HttpHandler
will be able to accept mocked request and dummy responder.
async/deferred.ts
Outdated
@@ -0,0 +1,27 @@ | |||
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. |
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 know about /async
... seems a bit too specific.
I feel like /util
would be a useful junk drawer for one-off things like this.
@hayd what do you think?
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 also considered to let it promise
FYI. I don't have any special reason to async
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.
Can you rename this to //util/deferred.ts
?
http/server.ts
Outdated
keys?: Key[]; | ||
handler: HttpHandler; | ||
}; | ||
} = Object.create(null); |
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 Map
would be better suited for this, you'd get rid of Object.*
methods
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.
Sure.
@keroxp could you also add example from Twitter to README? |
http/server.ts
Outdated
}; | ||
} | ||
|
||
async listen(addr: string, cancel: Deferred<void> = defer<void>()) { |
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 see 2 edge cases in this method:
- developer not calling
request.respond
inhandler
- no URL match
I think these situations would hang the request...
I'd opt to throw error if respond
is not called and return 404 response in respective situations
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.
Exactly. I will add fallbacks.
Just one last thing for discussion: I'm really not a fan of |
@keroxp Do you have a benchmark of this for |
@kevinkassimo No not yet. |
@keroxp FYI |
results in:
|
} | ||
|
||
export async function* serve(addr: string) { | ||
export async function* serve( |
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.
After some more investigation I'm skeptical about rewriting this function (minus readRequest
/writeResponse
bit) right now. We still need to figure out how to reuse connections and support keep-alive and this solution doesn't solve these problems. We should discuss and cross check in other languages.
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 reverted implementation of serve()
as possible. But race for cancellation may inevitably make performance degraded.
wrk resultsPerformance slightly degraded. master
latest
|
async/deferred.ts
Outdated
typeof x["resolve"] === "function" && | ||
typeof x["reject"] === "function" | ||
); | ||
} |
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.
Isn't this testing the same thing as the Deferred
type restriction itself? And it's not even used anywhere outside of the test file. 🤔
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.
It's custom type guard for Deferred
object. Mostly used to resolve it from union type.
@kevinkassimo @bartlomieju Could you run benchmark on your machine again? I don't know precisely how performance changed after reverting. |
@keroxp The req/sec seems very similar to master now on my machine. |
http/server.ts
Outdated
import { TextProtoReader } from "../textproto/mod.ts"; | ||
import { STATUS_TEXT } from "./http_status.ts"; | ||
import { assert } from "../testing/mod.ts"; | ||
import { defer, Deferred } from "../async/deferred.ts"; | ||
import { Key, pathToRegexp } from "./path_to_regexp.ts"; |
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 we should have fancy path matching in this module. Happy to have something like //http/router.ts
but in this server.ts
module things should be very simple.
In Go, ServeMux does not do such complex things: https://golang.org/pkg/net/http/#ServeMux
I would very happily have a ServeMux class here that implemented the above routing logic.
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.
@ry dropped path-to-regex, and implemented pure regexp based route matching
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'm still concerned that this is using RegExp rather than simple prefix matching... Is it possible to move this routing logic out of server.ts ?
server.ts is meant to be a very low-level web server, which everyone case use. In the case of the routing, I can imagine different people having different solutions.
I think the most complex routing inside server.ts should be limited to what is described here: https://golang.org/pkg/net/http/#ServeMux
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 that it is much complicated...routing with string or regex can be used for general purpose. serve()
should be considered as the lowest http server api.
Is it possible to move this routing logic out of server.ts ?
Of course. But even if logic become more simple, code won't be changed. We eventually call req.url.match(pattern)
. Differences are only storing match result in req
or not. I hesitate that we have two similar server api (ServerMux and Router).
Current:
master:
|
Please don't create a "util" 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.
LGTM - this is a great improvement - I don't want to hold it up any longer.
Regarding @hayd's comment on util... I'm going to land this now - but can we think about a better place for things like deferred()
? I agree "util" is quite generic (which I assume is @hayd's complaint) but I think "async" is too specific. I want to avoid having a very large number of directories in the root of deno_std - it should be less than, say, 100. But this requires that we carefully categorize modules, which is difficult. Any organizational help here would be appreciated. Renaming things now rather than later is better for stability.
The use of util means that /x/http/server.ts is broken atm. Putting it temporarily in http be okay, but using either |
I created a PR with promises/ #202 |
Since this PR caused chaos in libraries using |
Yeah - I'm in agreement. I skimmed some of the API changes here that I want to consider more carefully - also not so happy with the regex usage. |
Revert is landed. Sorry @keroxp - we need to break up this refactor into smaller bits - I'm happy to have improvements - but we need to more carefully consider the API changes. |
We need to consider the API changes here more carefully. This reverts commit da188a7. and commit 8569f15. Original: denoland/std@57c9176
HttpServer
type for general purpose http serverpath-to-regexp
moduleServerRequest
from class to interfaceserve()
cancelable