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

Paths fail to match when using regex match #3851

Open
lukaso opened this issue Jan 24, 2025 · 17 comments · Fixed by #3888
Open

Paths fail to match when using regex match #3851

lukaso opened this issue Jan 24, 2025 · 17 comments · Fixed by #3888
Labels

Comments

@lukaso
Copy link

lukaso commented Jan 24, 2025

What version of Hono are you using?

4.6.18

What runtime/platform is your app running on? (with version if possible)

Cloudflare workers but this happens in plain node as well

What steps can reproduce the bug?

Run the following:

import { Hono } from 'hono';

const app = new Hono({ strict: false });

app.get('/:first{.+}/middle-a/:reference?', (c) => {
  const first = c.req.param('first');
  const reference = c.req.param('reference');
  return c.text(first + ':' + reference);
});

app.get('/:first{.+}/middle-b/end-c/:uuid', async (c) => {
  return c.text('if this path enabled, and next path enabled, no paths work');
});

// if this path is commented out, then we get matching
app.get('/:first{.+}/middle-b/:digest', async (c) => {
  return c.text('if this path and last path enabled, no paths work');
});

let res = await app.request('/part1/part2/middle-a/latest');
console.log(await res.text()); // 404

res = await app.request('/part1/middle-b/end-c/latest');
console.log(await res.text()); // 404

https://stackblitz.com/edit/hono-regex?file=src%2Fapp.ts

What is the expected behavior?

The requests would succeed and return the result:

part1/part2:latest
if this path enabled, and next path enabled, no paths work

What do you see instead?

The requests fail and return the following:

404 Not Found
404 Not Found

Additional information

If you comment out either the second or third path, the remaining paths work. So it appears there is some internal conflict with the matcher that is affecting all paths.

I've noticed it doesn't affect other paths, and it doesn't affect any paths if the "greedy" matcher isn't used, namely {.+}.

I've run into this because it's a genuine requirement for a project I'm working on.

@lukaso lukaso added the triage label Jan 24, 2025
@EdamAme-x
Copy link
Contributor

EdamAme-x commented Jan 24, 2025

This is a temporary workaround, but you can avoid it by making the first greedy param part a wildcard and parsing it yourself or use Pattern Router or Linear Router.
I will do some research.

@EdamAme-x
Copy link
Contributor

Memo

  • RegExp Router: Unsupported
  • Trie Router: 404 (This is an issue)
  • Pattern Router, Linear Router: Works

@EdamAme-x
Copy link
Contributor

EdamAme-x commented Jan 24, 2025

This is a problem with Trie Router.

This is a radix tree.

gitpod@honojs-hono-qiqxbxc268s:/workspace/hono$ bun run sandbox/index.ts

// 1
{
  ":first{.+}": Node {
    _children: {
      "middle-a": Node {
        _children: {},
        insert: [Function: insert],
        search: [Function: search],
      },
    },
    insert: [Function: insert],
    search: [Function: search],
  },
}

// 2
{
  ":first{.+}": Node {
    _children: {
      "middle-a": Node {
        _children: {
          ":reference": Node {
            _children: {},
            insert: [Function: insert],
            search: [Function: search],
          },
        },
        insert: [Function: insert],
        search: [Function: search],
      },
    },
    insert: [Function: insert],
    search: [Function: search],
  },
}

// 3
{
  ":first{.+}": Node {
    _children: {
      "middle-a": Node {
        _children: {
          ":reference": Node {
            _children: {},
            insert: [Function: insert],
            search: [Function: search],
          },
        },
        insert: [Function: insert],
        search: [Function: search],
      },
      "middle-b": Node {
        _children: {
          "end-c": Node {
            _children: {
              ":uuid": Node {
                _children: {},
                insert: [Function: insert],
                search: [Function: search],
              },
            },
            insert: [Function: insert],
            search: [Function: search],
          },
        },
        insert: [Function: insert],
        search: [Function: search],
      },
    },
    insert: [Function: insert],
    search: [Function: search],
  },
}

// 4
{
  ":first{.+}": Node {
    _children: {
      "middle-a": Node {
        _children: {
          ":reference": Node {
            _children: {},
            insert: [Function: insert],
            search: [Function: search],
          },
        },
        insert: [Function: insert],
        search: [Function: search],
      },
      "middle-b": Node {
        _children: {
          "end-c": Node {
            _children: {
              ":uuid": Node {
                _children: {},
                insert: [Function: insert],
                search: [Function: search],
              },
            },
            insert: [Function: insert],
            search: [Function: search],
          },
          ":digest": Node {
            _children: {},
            insert: [Function: insert],
            search: [Function: search],
          },
        },
        insert: [Function: insert],
        search: [Function: search],
      },
    },
    insert: [Function: insert],
    search: [Function: search],
  },
}
404 Not Found
404 Not Found

@EdamAme-x
Copy link
Contributor

This may be spec...?

[ ":first{.+}", "first", /^.+$/ ] [ "part1", "part2", "middle-a", "latest" ] part1/part2/middle-a/latest
{
  first: "part1/part2/middle-a/latest",
}
404 Not Found
[ ":first{.+}", "first", /^.+$/ ] [ "part1", "middle-b", "end-c", "latest" ] part1/middle-b/end-c/latest
{
  first: "part1/middle-b/end-c/latest",
}
404 Not Found

@lukaso
Copy link
Author

lukaso commented Jan 24, 2025

Thanks so much for looking into this.

This is a temporary workaround, but you can avoid it by making the first greedy param part a wildcard and parsing it yourself or use Pattern Router or Linear Router.

I'm not really sure how to go about that. Like how do I know I'm using pattern or linear router?

Do you mean something like:

/*/middle-a/:reference? for all the patterns, and then grepping in the actual path?

A workaround I came up with was:

app.get('/:first{.+}/middle-b/:last{.+}', async (c) => {
  const lastbit = c.req.param('last').split('/');
  const isEndC = lastbit[0] === 'end-c';
  return c.text(isEndC.toString());
});

The problem is: I have quite a few of these patterns, and this was just the first manifestation.

@lukaso
Copy link
Author

lukaso commented Jan 24, 2025

I figured out how to use the routers.

So LinearRouter and TrieRouter have the same problem, RegExpRouter gives UnsupportedPathError and PatternRouter works.

@EdamAme-x
Copy link
Contributor

In my opinion, not supporting overly complex path expressions can be a compromise to preserve performance.

@EdamAme-x
Copy link
Contributor

I hope someone will make a third party pre-set for users to create their own routers (this may or may not be a joke

@lukaso
Copy link
Author

lukaso commented Jan 24, 2025

In my opinion, not supporting overly complex path expressions can be a compromise to preserve performance.

A few questions:

  • Is it your view that this particular use case is overly complicated? Or is that a more general sentiment. I agree it's complicated, but for me it isn't "overly complicated".

  • Based on your comments, is this a use case Hono won't support and therefore I should use another web framework?

  • Is this use case causing Hono to be slow?

To explain why these routes are needed: this structure supports the docker and OCI specifications, which I have no control over. So for me, the routes are as complicated as they have to be. :)

Finally, whatever the outcome, in my view the router should error rather than silently fail. There's also no indication in the docs that this doesn't work (or at least I didn't find it).

@yusukebe
Copy link
Member

Hi @lukaso

Thank you for the issue and comment. I haven't looked at it well yet, but we have to fix the behaviors that are not unified between routers. I'll work on it, or someone can help it.

@yusukebe yusukebe added bug and removed triage labels Jan 24, 2025
@EdamAme-x
Copy link
Contributor

We need to continue our research a little longer.

@EdamAme-x
Copy link
Contributor

EdamAme-x commented Jan 24, 2025

Is it your view that this particular use case is overly complicated? Or is that a more general sentiment. I agree it's complicated, but for me it isn't "overly complicated".

I should not have said “overly complicated” there. Sorry.

Based on your comments, is this a use case Hono won't support and therefore I should use another web framework?

No, there are still other workarounds.

Is this use case causing Hono to be slow?

The fact that router supports complex paths means that Hono itself is slower. (Although this may not have much of an impact overall)

@lukaso
Copy link
Author

lukaso commented Jan 24, 2025

Thanks both! And thank you for all your efforts with Hono.

@yusukebe
Copy link
Member

yusukebe commented Feb 2, 2025

Hi @usualoma

What do you think of this issue? Either way, I think we should fix the difference behavior between routers as @EdamAme-x pointed.

@usualoma
Copy link
Member

usualoma commented Feb 4, 2025

RegExpRouter does support greedy matching. However, in the example of the first comment, it is "Unsupported" due to the combination of the three paths.

I think that the following changes to TrieRouter and LinearRouter would allow them to support "greedy match" (when static components follow), but should we do it?
#3888

@yusukebe
Copy link
Member

yusukebe commented Feb 4, 2025

Hi @usualoma

Thank you for the PR. That change will be good. But the TrieRouter in the PR still does not support the following pattern, which @lukaso showed first.

router.add('GET', '/:first{.+}/middle-a/:reference?', '1')
router.add('GET', '/:first{.+}/middle-b/end-c/:uuid', '2')
router.add('GET', '/:first{.+}/middle-b/:digest', '3')

const res = router.match('GET', '/part1/middle-b/end-c/latest')
console.log(res[0][0][0]) // should be 2 but TrieRouter does not match any routes

I think it's good the routers without RegExpRouter can support the expected behavior of this issue.

@usualoma
Copy link
Member

usualoma commented Feb 4, 2025

Hi @yusukebe

Sorry, I think the problem has been fixed by
2fa716f

It doesn't increase the amount of code, and I feel this is a good change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants