Closed yusukebe closed 2 years ago
How about this:
Basically, to make the route as a handler, use c.HTTP_METHOD
.
app.get(path, handler)
---> app.get('/a', (c) => c.text('a))
To make the route as middleware, use c.use
.
app.use(path, middleware)
---> app.use('/a', cors())
If we want to specify HTTP method for applying middleware, use c.HTTP_METHOD
.
app.get('/a', middleware, middleware.., handler)
---> app.post('/a', basicAuth(), bodyParse(), (c) => c.text('a)')
The order is important. Last route will be handler, others will be middlware.
Hi! @yusukebe
Just to be sure, are you saying that we should still have considered the matter I am talking about in this comment?
https://github.com/honojs/hono/issues/189#issuecomment-1121693657
I agree with the specifications you wrote above. This was exactly what I wanted to suggest in my comments below. Sorry if I did not communicate it well.
https://github.com/honojs/hono/issues/189#issuecomment-1120625620
The implementation would be as follows: "If multiple handlers are detected, only the last one will be executed."
(It would be best if it could be handled within router class, as it would be costly to do so with every dispatch
call.)
diff --git a/src/hono.test.ts b/src/hono.test.ts
index c241140..80f70cd 100644
--- a/src/hono.test.ts
+++ b/src/hono.test.ts
@@ -432,7 +432,7 @@ describe('Middleware with app.HTTP_METHOD', () => {
describe('Basic', () => {
const app = new Hono()
- app.all('*', async (c, next) => {
+ app.use('*', async (c, next) => {
await next()
c.header('x-custom-message', 'hello')
})
diff --git a/src/hono.ts b/src/hono.ts
index 62e5a10..230b5d9 100644
--- a/src/hono.ts
+++ b/src/hono.ts
@@ -31,10 +31,14 @@ declare global {
}
}
-export type Handler<RequestParamKeyType extends string = string, E = Env> = (
- c: Context<RequestParamKeyType, E>,
- next: Next
-) => Response | Promise<Response> | void | Promise<void>
+export type Handler<RequestParamKeyType extends string = string, E = Env> = {
+ (c: Context<RequestParamKeyType, E>, next: Next):
+ | Response
+ | Promise<Response>
+ | void
+ | Promise<void>
+ isHandler?: boolean
+}
export type NotFoundHandler<E = Env> = (c: Context<string, E>) => Response
export type ErrorHandler<E = Env> = (err: Error, c: Context<string, E>) => Response
export type Next = () => Promise<void>
@@ -101,15 +105,17 @@ export class Hono<E = Env, P extends string = '/'> extends defineDynamicClass()<
allMethods.map((method) => {
this[method] = <Path extends string>(
args1: Path | Handler<ParamKeys<Path>, E>,
- ...args: [Handler<ParamKeys<Path>, E>]
+ ...args: Handler<ParamKeys<Path>, E>[]
) => {
if (typeof args1 === 'string') {
this.path = args1
} else {
+ args1.isHandler = args.length === 0
this.addRoute(method, this.path, args1)
}
- args.map((handler) => {
+ args.map((handler, i) => {
if (typeof handler !== 'string') {
+ handler.isHandler = i === args.length - 1
this.addRoute(method, this.path, handler)
}
})
@@ -202,6 +208,18 @@ export class Hono<E = Env, P extends string = '/'> extends defineDynamicClass()<
}
}) as typeof request.param
const handlers = result ? result.handlers : [this.notFoundHandler]
+ if (result) {
+ for (let i = handlers.length - 1, hasHandler = false; i >= 0; i--) {
+ if (!hasHandler) {
+ hasHandler = (handlers[i] as Handler).isHandler
+ continue
+ }
+
+ if ((handlers[i] as Handler).isHandler) {
+ handlers.splice(i, 1)
+ }
+ }
+ }
const c = new Context<string, E>(request, {
env: env,
Hi @usualoma
Just to be sure, are you saying that we should still have considered the matter I am talking about in this comment?
Yes. I'm sorry too for not understanding enough.
I agree with the specifications you wrote above.
Thank you!
The implementation would be as follows: "If multiple handlers are detected, only the last one will be executed." (It would be best if it could be handled within router class, as it would be costly to do so with every dispatch call.)
I like your implementation. It's simple and covers all patterns. I think we don't have to rewrite routers. Let’s go with it!
Could you create the pull request for this code?
Determine handler or middleware.
To be honest, I think there should be there're (or should be) no differences between hander and middleware, or we can say the handler
is just a response middleware.
Both of them are async functions, compose
function make a process pipeline of them for the incoming fetch event, each unit (middleware) does a simple operation on the context (req, headers, res), and response a result to the client, the whole process is simple and beautiful.
To determine a function is handler or middleware, requires us to put extra information onto the functions, which break the simplicity but without obvious benefit.
Make only one handler invoked.
When functions are registered for a path, I think they should be all invoked by order since that's what the code designer want.
The issue you guys mentioned in https://github.com/honojs/hono/issues/258, I think it's not about middleware or handler stuff, it's about path matching algorithm.
app.get('/:slug', m2(), m3());
app.get('/a', m0(), m1());
it's about how we think about /a
and /:slug
, since we think /a
is a special case for /:slug
, then [m0, m1] is for /a
, and [m2, m3] is for /:slug
.
@metrue Thanks!
I had overlooked that case.
app.get('/:slug', m2(), m3());
app.get('/a', m0(), m1());
I agree with the specification that the routing results should be as follows
[m0, m1] is for
/a
, and [m2, m3] is for/:slug
.
The patch above is not good enough for that.
However, I still think that internally we have to distinguish between a middleware or a handler.
app.use('/posts/*', a); // a is a middleware
app.get('/posts/*', b); // b is a handler
app.get('/posts/a', c); // c is a handler
I don't think we are compromising the simplicity of compose.ts here. I think the information about whether it is a handler or middleware can be treated as information for routing purposes.
I think the following implementation could solve the problem. (there is much room for performance improvement)
https://github.com/honojs/hono/compare/master...usualoma:ensure-one-handler-2
@usualoma You codes look nice, but I would like to share a little more about this middleware&handler concept.
I know we're doing differently on router
(I mean the router object: router.ts) with Koa and Express, they make router just a middleware.
But the design philosophy is worth learning: An Express/Koa application is essentially a series of middleware function calls, with this fundamental concept, things get easy: users register the functions for endpoints one by one which they wish to be invoked in order for a incoming request; The frameworks get the incoming request, compose the context, invoke all the functions and response context's res.
Let's take a look on your example,
app.use('/posts/*', a);
app.get('/posts/*', b);
app.get('/posts/a', c);
Above codes actually tells the route matching attention:
a
should be invoked for path /post/*
on all HTTP methods.GET /posts/*
with a process pipeline with only on function b
.GET /post/a' with a process pipeline with only on function
b`.So we can see a
, b
, and c
are just functions without any differences on matching and invoking level,
Hi @metrue @usualoma !
from @metrue comment: there should be there're (or should be) no differences between hander and middleware
I really think so. We want to write not only using c.use
but like this:
app.get('/', md1) // <--- dispatch middleware GET /
I think this issue is just about implementation.
Sorry to go back to issue #189. I've noticed we have a simple way to solve this issue. Use Routing Order Specification proposed @usualoma https://github.com/honojs/hono/issues/189#issuecomment-1121631592
Koa/Express is using this way too.
If we remove this one line from current code:
diff --git a/src/compose.ts b/src/compose.ts
index fb49e13..c35bf45 100644
--- a/src/compose.ts
+++ b/src/compose.ts
@@ -32,7 +32,6 @@ export const compose = <C>(
if (res && context instanceof Context) {
context.res = res
context.res.finalized = true
- await dispatch(i + 1) // <--- Call next()
}
return context
})
and, run the tests below:
const handler1 = (c: Context) => {
console.log('handler1')
return c.text('handler1')
}
const handler2 = (c: Context) => {
console.log('handler2') // <--- this log will no be emmited
return c.text('handler2')
}
const composed = compose([md1, md2, handler1, handler2])
await composed(c)
expect(await c.res.text()).toBe('handler1')
handler1
will be dispatched but, handler2
will not be dispatched because
This behavior is expected.
But, by current implementation, this tests are failed:
app.get('/posts/:id', (c) => {
return c.text('first')
})
app.get('/:type/:id', (c) => {
return c.text('second')
})
it('Should return response from `specialized` route', async () => {
const res = await app.request('http://localhost/posts/123')
expect(await res.text()).toBe('first') // <--- Failed!!! to be 'second'
})
Because sort order depends on the router's behavior, it is decided by how it is specialized.
If use Routing Order Specification by @usualoma :
it will be more simple. If we want to dispatch handler a
, simply put a
above.
Then, we can write like a Koa/Express, @metrue said.
I'm really sorry to go back to the story, but I think this is a good way.
Koa/Express is using this way too.
Um, I think..., in the Express, the call order depends only on the order of registration, and the content of the path has no effect on the call order at all. (I have not tested in the Koa.)
If we call the functions in the order of registration and stop processing the compose function when the Response
object is returned, I think we will get the same behavior.
for http://localhost/a
...
app.use('/posts/*', function a(c, next) { next() });
app.get('/posts/*', function b(c, next) { next() });
app.get('/posts/:slug', function c(c) { return c.text('slug') });
app.get('/posts/a', function d(c) => { return c.text('a') });
app.get('/*', function e(c, next) { next() });
=> a => b => c (d, e should not be called)
app.use('/posts/*', function a(c, next) { next() });
app.get('/posts/*', function b(c, next) { next() });
app.get('/posts/a', function d(c) => { return c.text('a') });
app.get('/posts/:slug', function c(c) { return c.text('slug') });
app.get('/*', function e(c, next) { next() });
=> a => b => d (c, e should not be called)
app.get('/posts/a', function d(c) => { return c.text('a') });
app.get('/posts/:slug', function c(c) { return c.text('slug') });
app.use('/posts/*', function a(c, next) { next() });
app.get('/posts/*', function b(c, next) { next() });
app.get('/*', function e(c, next) { next() });
=> d (a, b, e and c should not be called)
We might want to look back at this Express specification as well and consider whether the https://github.com/honojs/hono/issues/189#issuecomment-1121631592 specification is what we want.
I tested with express 4.18.1.
const express = require('express')
const app = express()
const port = 3000
app.use('/posts/*', (req, res, next) => {
console.log('middleware');
next();
})
app.get('/posts/:slug', (req, res, next) => {
console.log('slug');
next();
})
app.get('/posts/abc', (req, res, next) => {
console.log('abc');
next();
})
app.get('/*', (req, res) => {
res.send('content')
})
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
});
Hmm, that's right.
To say simply, we want to do this: right?
app.get('/*', async (_c, next) => { await next()}) // <--- a
app.get('/:slug', (c) => { return c.text('/:slug') }) // <--- b
app.get('/page', (c) => { return c.text('/page') }) // <--- c
GET /page ---> a => c
GET /123 ---> a => b
app.get('/*', async (_c, next) => { await next()}) // <--- a
app.get('/:slug', (c) => { return c.text('/:slug') }) // <--- b
app.get('/page', (c) => { return c.text('/page') }) // <--- c
GET /page ---> a => c
GET /123 ---> a => b
I have different expectation, I think it should be
GET /page ---> c
GET /123 ---> b
GET /xxx ---> a invoked, but res is initial value since it not set in any middleware
Based on the ideas:
.use
(or .get
, .post
, .delete
, etc) register the middlewares one by one in order, invoking order is same with registering order.@metrue Hi
If you want to dispatch logger
in every path, how can you write?
I want to write like this:
app.get('/*', logger()) // <--- logger
app.get('/:slug', (c) => { return c.text('/:slug') }) // <--- b
app.get('/page', (c) => { return c.text('/page') }) // <--- c
GET /page ---> logger + c
GET /123 ---> logger + b
@yusukebe
It would be,
app.use(logger())
Since no path and method specified, the logger()
middleware will be pushed into global middleware queue, which means all the incoming fetch event will go through it.
app.get('/:slug', (c) => { return c.text('/:slug') }) // <--- b
Register b
middleware for GET /:slug
, its middlewares list become [logger(), b()]
[logger(), b()] = [global middlewares] + [endpoint middlewares]
app.get('/page', (c) => { return c.text('/page') }) // <--- c
Register c
for specific endpoint GET /page
, its middlwares list became [logger(), c()]
[logger(), c()] = [global middlewares] + [specific endpoint middlewares]
How about something like this? https://github.com/honojs/hono/compare/master...usualoma:update-router-interface
Response
object is returned from a handler, do not process the rest.In the branch above, the default is the same rules as the Express. This is not because this is preferred, but because it is easier to implement.
// sort by https://github.com/honojs/hono/issues/189#issuecomment-1121631592
const app = new Hono({
router: new TrieRouter({
scorer: (() => {
let index = 0
return (path) => `${(path.match(/\/(?::\w+{[^}]+}|[^\/]*)/g) || []).length}.${index++}`
})(),
}),
})
app.get('*', function a() {})
app.get('*', function b() {})
app.get('/*', function c() {})
app.get('/api', function d() {})
app.get('/api/:type/:id', function e() {})
app.get('/api/posts/:id', function f() {})
app.get('/*/*/:id', function g() {})
app.request('http://localhost/api/posts/1')
// a => b => c => e => f => g
// sort by level of detail
const app = new Hono({
router: new TrieRouter({
scorer: (path) => {
if (path === '*' || path === '/*') {
return '0'
}
const components = path.match(/\/(?::\w+{[^}]+}|[^\/]*)/g) || []
const paramIndexList: number[] = []
for (let i = 0, len = components.length; i < len; i++) {
if (/\/(?::|\*)/.test(components[i])) {
paramIndexList.push(i)
}
}
return [components.length]
.concat(paramIndexList)
.map((i) => String(999 - i).padStart(3, '0'))
.join('')
},
}),
})
app.get('*', function a() {})
app.get('*', function b() {})
app.get('/*', function c() {})
app.get('/api', function d() {})
app.get('/api/:type/:id', function e() {})
app.get('/api/posts/:id', function f() {})
app.get('/*/*/:id', function g() {})
app.request('http://localhost/api/posts/1')
// a => b => c => f => e => g
Oh, I'm thinking the same thing, and writing the code for this feature 😆 https://github.com/honojs/hono/compare/master...router-ordering
In my implementation, the sorting rule is like below / maybe almost are as same as the code by @usualoma
describe('Complex', () => {
const node = new Node()
node.insert('get', '/api/*', 'c') // score 2.1
node.insert('get', '/api/:type/:id', 'd') // score 3.2
node.insert('get', '/api/posts/:id', 'e') // score 3.3
node.insert('get', '/api/posts/123', 'f') // score 3.4
node.insert('get', '/*/*/:id', 'g') // score 3.5
node.insert('get', '/api/posts/*/comment', 'h') // score 4.6 - not match
node.insert('get', '*', 'a') // score 1.7
node.insert('get', '*', 'b') // score 1.8
it('get /api/posts/123', async () => {
const res = node.search('get', '/api/posts/123')
// ---> will match => c, d, e, f, b, a, b
// ---> sort by score => a, b, c, d, e, f, g
expect(res.handlers).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g'])
})
})
I agree with your opinion!
I have confirmed https://github.com/honojs/hono/compare/master...router-ordering!
It was simpler than mine and I felt it was implemented well enough for we needs. There was also a test. (There was no test in my branch) I hope we can proceed based on your branch.
@usualoma Thank you for confirming.
Hi @metrue
I want to go with this. The koa/express design might be nice, but I like this one. And it's not so different. I'll try to summarize it.
Handler and Middleware are almost the same. The differences are below:
Response
object.next()
The user can register middlewares using c.use
or register these using c.HTTP_METHOD
as well as the handlers. For this feature, it's easy to specify the path and the method.
// match any method, all routes
app.use('*', logger())
// specify path
app.use('/posts/*', cors())
// specify method and path
app.post('/posts/*', basicAuth(), bodyParse())
If the handler returns Response
, it will be used for the end-user, stopping the processing.
app.post('/posts', (c) => c.text('Created!', 201))
In this case, four middlewares are processed before dispatching like this:
logger() -> cors() -> basicAuth() -> bodyParse() -> *handler*
Matching priority is decided by ordering. The handler that is registered above will be dispatched.
app.get('/book/a', (c) => c.text('a')) // a
app.get('/book/:slug', (c) => c.text('common')) // common
GET /book/a ---> `a` // common will not be dispatched
GET /book/b ---> `common` // a will not be dispatched
All scoring rules:
app.get('/api/*', 'c') // score 2.1
app.get('/api/:type/:id', 'd') // score 3.2
app.get('/api/posts/:id', 'e') // score 3.3
app.get('/api/posts/123', 'f') // score 3.4
app.get('/*/*/:id', 'g') // score 3.5
app.get('/api/posts/*/comment', 'h') // score 4.6 - not match
app.get('*', 'a') // score 1.7
app.get('*', 'b') // score 1.8
GET /api/posts/123
---> will match => c, d, e, f, b, a, b
---> sort by score => a, b, c, d, e, f, g
That's all! I think you'll like it too. So I want to go with this.
I think the following additional explanation is needed.
/*
Scores may be dynamically determined in the following cases.
app.get('/api/*', 'auth middleware') // score 1.1 or 2.1 ?
app.get('/api', 'b') // score 1.2
app.get('/api/posts', 'c') // score 2.3
app.get('/api/*', 'fallback') // score 1.4 or 2.4 ?
Scores for routings ending with /*
are calculated using the following.
`${Math.min(slash_count_of_request_path, slash_count_of_route_path}.${registration_order}`
The results are as follows.
GET /api
--->
app.get('/api/*', 'authentication') // score 1.1
app.get('/api', 'b') // score 1.2
app.get('/api/posts', 'c') // score 2.3 - not match
app.get('/api/*', 'fallback') // score 1.4
GET /api/posts
--->
app.get('/api/*', 'authentication') // score 2.1
app.get('/api', 'b') // score 1.2 - not match
app.get('/api/posts', 'c') // score 2.3
app.get('/api/*', 'fallback') // score 2.4
@yusukebe
In the router-ordering branch, the following code failed.
describe('Blog - failed', () => {
const router = new TrieRouter<string>()
router.add('POST', '/entry', 'post entry')
router.add('POST', '/entry/*', 'fallback')
router.add('GET', '/entry/:id', 'get entry') // success if this line is commented out
it('POST /entry', async () => {
const res = router.match('POST', '/entry')
expect(res).not.toBeNull()
expect(res.handlers).toEqual(['post entry', 'fallback'])
})
})
I think the order property is being overwritten at the following location. https://github.com/honojs/hono/blob/1fe87ece320e815b31fba328d7fe946b09314098/src/router/trie-router/node.ts#L75
Hi @usualoma
I've pushed perfect code into https://github.com/honojs/hono/compare/master...router-ordering Fixed problem you said.
There is another matter. It's a little bit tricky but, we have to implement special wildcard.
In this case, /entry/*
tasks priority over /entry
. It's expected.
describe('Blog - special Wildcard', () => {
const router = new TrieRouter<string>()
router.add('POST', '/entry', 'post entry') // 1.01
// /entry/* will match /entry*
router.add('POST', '/entry/*', 'special') // 1.00
it('POST /entry', async () => {
const res = router.match('POST', '/entry')
expect(res.handlers).toEqual(['special', 'post entry'])
})
})
Because, there is a case to apply middleware both /entry
/entry/abc
by /entry/*
. This is already implemented in the current master branch. https://github.com/honojs/hono/blob/52bca3bd0a3b14cae861d47c47437821768b6686/src/router/trie-router/node.ts#L149
Can you implement it with RegExpRouter?
@yusukebe I think that it is harder to write fallback routing when the following score is set
router.add('POST', '/entry', 'post entry') // 1.01
router.add('POST', '/entry/*', 'special') // 1.00
As for the handling of special wildcard
, I think the way shown in https://github.com/honojs/hono/issues/262#issuecomment-1135232294 is better, what do you think?
@usualoma
I agree with your scoring rule. I've fixed the code and pushed it! https://github.com/honojs/hono/compare/master...router-ordering
@yusukebe Thank you, I also implemented the RegExpRouter version in https://github.com/honojs/hono/pull/267.
As an aside...
/*
ScoringI think we can think of scoring against /*
as static, rather than as the dynamic we talked about above.
app.get('/entry/*', function a() {})
This can be taken to mean the following
app.get('/entry', function a() {})
app.get('/entry/{accept all remaining components}', function a'() {})
In this light.
app.get('/api/*', function a() {})
app.get('/api', function b() {})
app.get('/api/posts', function c() {})
app.get('/api/*', function d() {})
This means the following
app.get('/api', function a() {}) // score 1.1
app.get('/api/{accept all remaining components}', function a'() {}) // score 2.2
app.get('/api', function b() {}) // score 1.3
app.get('/api/posts', function c() {}) // score 2.4
app.get('/api', function d() {}) // score 1.5
app.get('/api/{accept all remaining components}', function d'() {}) // score 2.6
GET /api
--->
a() => b() => d()
GET /api/posts
--->
a'() => c() => d'()
Hi @usualoma
Thank you for the PR, I will merge it from now on and will release v1.4.0 including this feature.
I think we can think of scoring against /* as static, rather than as the dynamic we talked about above.
Yes, I know it. It's expected behavior. That's OK!
@yusukebe
I know codes tell everything but it's also great if we put the whole design into README.md or just comments in hono.ts to explain the whole algorithm, just a nice to have.
I think I like everything what you guys are trying to do, except this one,
If the handler returns Response, it will be used for the end-user, stopping the processing.
hono.get(
'*',
(ctx, next) => {
console.log('a1')
next()
console.log('a2')
},
(ctx, next) => {
console.log('b1')
next()
console.log('b2')
},
(ctx, next) => {
ctx.json('ok1')
console.log('ok1')
next()
},
(ctx) => {
return ctx.json('ok2')
},
)
What's the output if we GET /
? is it a1 -> b1 -> ok1 -> b2 -> a2
and the response is ok2
?
@metrue
What's the output if we GET / ? is it a1 -> b1 -> ok1 -> b2 -> a2 and the response is ok2?
Yes!
@yusukebe Excellent, it's my expectation. Coooool.
This just bit me! Can we expect a release soon :)? 🚀
Hi @dan-lee
Released just now! https://github.com/honojs/hono/releases/tag/v1.4.0
@yusukebe Thank you!
Maybe I misunderstood the point of this issue, I've got the following use case:
test('graphql/catch all', () => {
const router = new TrieRouter<string>()
router.add('ALL', '/graphql', 'graphql')
router.add('ALL', '/*', 'everything else')
const res = router.match('POST', '/graphql')
expect(res.handlers).toBe(['graphql'])
})
I expected only /graphql
to match and not /*
, as it is a fallback/catch-all.
Instead, both handlers seem to match:
- Expected - 0
+ Received + 1
Array [
+ "everything else",
"graphql",
]
For me this is unexpected because I am returning a response in /graphql
and no other routes should do any work from then on.
Hi @dan-lee !
We've released v1.4.1. https://github.com/honojs/hono/releases/tag/v1.4.1
This release includes the feature that you expected. Update routing priority and fixed the issue about GraghQL server middleware. The following code works well. Please check it out!
import { buildSchema } from 'graphql'
import { Hono } from 'hono'
import { graphqlServer } from 'hono/graphql-server'
const app = new Hono()
const schema = buildSchema(`
type Query {
hello: String
}
`)
const rootValue = {
hello: () => 'Hello Hono!',
}
app.use(
'/graphql',
graphqlServer({
schema,
rootValue,
})
)
app.get('*', (c) => {
return c.text('fallback')
})
export default app
Hey @yusukebe, thanks a lot for your great work! It works perfectly now 🤩
In the Iusse #189 , we decided to treat handlers and middleware as the same. We can write like these:
This is good.
How about Handler + Handler pattern.
This Response body will be
a
. This is expected.BUT, in #258 I notice that every handler is processed.
console.log
emitsslug
,a
.The reason:
I think this design is not so good. Only one handler should be invoked. In this case,
console.log
should emit onlya
. So, we have to:How can we do that?