Skip to content

Downstream methods without path args break upstream method path typing #4517

@ambergristle

Description

@ambergristle

What version of Hono are you using?

4.10.5

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

Bun 1.2.13

What steps can reproduce the bug?

a typing edge case was reported on discord

TL;DR

  • when a handler or middleware is called with a path argument, the Hono instance this.#path is updated
    • if the next method in the chain is also called with the path argument, there's no issue
    • if the next method is called without the path argument, then:
      • the call defaults the path to the previously-set this.#path (expected)
      • the schema uses the previously-set path type (expected)
      • any methods called before the middleware are also typed using the not-yet-set path (unexpected)
  • i don't totally understand why this happens, but i think has to do with the type merging that ChangePathOfSchema does

Test Case

  • the expected endpoints are GET / and POST /:id
  • the actual endpoints are GET / and POST /:id
  • the endpoint types are GET /:id and POST /:id
type Env = {
  Variables: {
    foo: string
  }
}

const app = new Hono<Env>()
  .get((c) => c.text('before'))
  .use('/:id', async (_c, next) => {
    await next()
  })
  .post((c) => c.text('after'))

type Actual = ExtractSchema<typeof app>
/**
  {
    '/:id': {
      $get: {
        input: {};
        output: 'before';
        outputFormat: 'text';
        status: ContentfulStatusCode;
      }
    }
  } & {
    '/:id': {
      $post: {
        input: {
          param: {
            id: string;
          };
        };
        output: 'after';
        outputFormat: 'text';
        status: ContentfulStatusCode;
      }
    }
  }
*/
type Expected = {
  '/': {
    $get: {
      input: {};
      output: 'before';
      outputFormat: 'text';
      status: ContentfulStatusCode;
    }
  }
} & {
  '/:id': {
    $post: {
      input: {
        param: {
          id: string;
        };
      };
      output: 'after';
      outputFormat: 'text';
      status: ContentfulStatusCode;
    }
  }
}

// error
type verify = Expect<Equal<Expected, Actual>>

What is the expected behavior?

No response

What do you see instead?

No response

Additional information

ideally the types would be in sync with the endpoint behavior. that being said, the current solution covers a majority of cases, and i'm not sure there's a performant solution with better coverage. there are a few simple rules/workarounds

  • avoid calling methods/use without the path argument
  • front-load middleware at the top of the chain
  • break routes up RESTfully

Example Workaround

const idRoute = new Hono<Env>()
  .use('/:id', async (_c, next) => {
    await next()
  })
  .post('/:id', (c) => c.text('after'))

const app = new Hono<Env>()
  .get('/', (c) => c.text('before'))
  .route('/', idRoute)

Next Steps

  • i'll update the docs to describe the path defaulting behavior when methods are called without a path argument
    • i'll include a warning about this edge-case
  • i'll work on a solution when i have a bit more time, but i'm wondering whether it's worth trying to resolve at all right now

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions