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

422 Unprocessable Content on official tutorial code - cookie 'token' missing #1144

Open
Tiv0w opened this issue Mar 27, 2025 · 2 comments
Open
Labels
bug Something isn't working

Comments

@Tiv0w
Copy link

Tiv0w commented Mar 27, 2025

What version of Elysia is running?

latest (1.2.25) - bun 1.2.6

What platform is your computer?

Linux 6.13.7-arch1-1 x86_64 unknown

What steps can reproduce the bug?

  • Create a new Elysia project using:
bun create elysia bug-repro
// src/index.ts

import { Elysia } from 'elysia'
import { swagger } from '@elysiajs/swagger'
import { opentelemetry } from '@elysiajs/opentelemetry'

import { note } from './note'
import { user } from './user'

const app = new Elysia()
    .use(opentelemetry())
    .use(swagger())
    .onError(({ error, code }) => {
        if (code === 'NOT_FOUND') return 'Not Found :('

        console.error(error)
    })
    .use(user)
    .use(note)
    .listen(3000)
// src/user.ts

import { Elysia, t } from 'elysia'

export const userService = new Elysia({ name: 'user/service' })
    .state({
        user: {} as Record<string, string>,
        session: {} as Record<number, string>
    })
    .model({
        signIn: t.Object({
            username: t.String({ minLength: 1 }),
            password: t.String({ minLength: 8 })
        }),
        session: t.Cookie(
            {
                token: t.Number()
            },
            {
                secrets: 'seia'
            }
        ),
        optionalSession: t.Optional(t.Ref('session'))
    })
    .macro({
        isSignIn(enabled: boolean) {
            if (!enabled) return

            return {
            	beforeHandle({ error, cookie: { token }, store: { session } }) {
                    if (!token.value)
                        return error(401, {
                            success: false,
                            message: 'Unauthorized'
                        })

                    const username = session[token.value as unknown as number]

                    if (!username)
                        return error(401, {
                            success: false,
                            message: 'Unauthorized'
                        })
                }
            }
        }
    })

export const getUserId = new Elysia()
    .use(userService)
    .guard({
    	isSignIn: true,
        cookie: 'session'
    })
    .resolve(({ store: { session }, cookie: { token } }) => ({
        username: session[token.value]
    }))
    .as('plugin')

export const user = new Elysia({ prefix: '/user' })
    .use(userService)
    .put(
        '/sign-up',
        async ({ body: { username, password }, store, error }) => {
            if (store.user[username])
                return error(400, {
                    success: false,
                    message: 'User already exists'
                })

            store.user[username] = await Bun.password.hash(password)

            return {
                success: true,
                message: 'User created'
            }
        },
        {
            body: 'signIn'
        }
    )
    .post(
        '/sign-in',
        async ({
            store: { user, session },
            error,
            body: { username, password },
            cookie: { token }
        }) => {
            if (
                !user[username] ||
                !(await Bun.password.verify(password, user[username]))
            )
                return error(400, {
                    success: false,
                    message: 'Invalid username or password'
                })

            const key = crypto.getRandomValues(new Uint32Array(1))[0]
            session[key] = username
            token.value = key

            return {
                success: true,
                message: `Signed in as ${username}`
            }
        },
        {
            body: 'signIn',
            cookie: 'optionalSession'
        }
    )
    .get(
        '/sign-out',
        ({ cookie: { token } }) => {
            token.remove()

            return {
                success: true,
                message: 'Signed out'
            }
        },
        {
            cookie: 'optionalSession'
        }
    )
    .use(getUserId)
    .get('/profile', ({ username }) => ({
        success: true,
        username
    }))
// src/note.ts

import { Elysia, t } from 'elysia'
import { getUserId, userService } from './user'

const memo = t.Object({
    data: t.String(),
    author: t.String()
})

type Memo = typeof memo.static

class Note {
    constructor(
        public data: Memo[] = [
            {
                data: 'Moonhalo',
                author: 'saltyaom'
            }
        ]
    ) {}

    add(note: Memo) {
        this.data.push(note)

        return this.data
    }

    remove(index: number) {
        return this.data.splice(index, 1)
    }

    update(index: number, note: Partial<Memo>) {
        return (this.data[index] = { ...this.data[index], ...note })
    }
}

export const note = new Elysia({ prefix: '/note' })
    .use(userService)
    .decorate('note', new Note())
    .model({
        memo: t.Omit(memo, ['author'])
    })
    .onTransform(function log({ body, params, path, request: { method } }) {
        console.log(`${method} ${path}`, {
            body,
            params
        })
    })
    .get('/', ({ note }) => note.data)
    .use(getUserId)
    .put(
        '/',
        ({ note, body: { data }, username }) =>
            note.add({ data, author: username }),
        {
            body: 'memo'
        }
    )
    .get(
        '/:index',
        ({ note, params: { index }, error }) => {
            return note.data[index] ?? error(404, 'Not Found :(')
        },
        {
            params: t.Object({
                index: t.Number()
            })
        }
    )
    .guard({
        params: t.Object({
            index: t.Number()
        })
    })
    .delete('/:index', ({ note, params: { index }, error }) => {
        if (index in note.data) return note.remove(index)

        return error(422)
    })
    .patch(
        '/:index',
        ({ note, params: { index }, body: { data }, error, username }) => {
            if (index in note.data)
                return note.update(index, { data, author: username })

            return error(422)
        },
        {
            isSignIn: true,
            body: 'memo'
        }
    )
  • Add the necessary dependencies used by the tutorial:
bun add --dev @elysiajs/swagger @elysiajs/opentelemetry
  • Run bun dev

  • Go to http://localhost:3000/swagger

  • Create a user through the sign-up endpoint (I used name as name and password as password) (works)

  • Try to sign in through the sign-in endpoint with the correct credentials.

What is the expected behavior?

The user should be logged in, the token cookie should be set in the response, and should NOT be required at the request level (otherwise there's no way to log in without an already active session).

What do you see instead?

A 422 Unprocessable Content error response with no cookie set and the following body:

{
  "type": "validation",
  "on": "cookie",
  "summary": "Property 'token' is missing",
  "property": "/token",
  "message": "Expected required property",
  "expected": {
    "token": 0
  },
  "found": {},
  "errors": [
    {
      "type": 45,
      "schema": {
        "anyOf": [
          {
            "format": "numeric",
            "default": 0,
            "type": "string"
          },
          {
            "type": "number",
            "anyOf": [
              {
                "type": "string",
                "format": "numeric",
                "default": 0
              },
              {
                "type": "number",
                "anyOf": [
                  {
                    "type": "string",
                    "format": "numeric",
                    "default": 0
                  },
                  {
                    "type": "number",
                    "anyOf": [
                      {
                        "type": "string",
                        "format": "numeric",
                        "default": 0
                      },
                      {
                        "type": "number",
                        "anyOf": [
                          {
                            "type": "string",
                            "format": "numeric",
                            "default": 0
                          },
                          {
                            "type": "number",
                            "anyOf": [
                              {
                                "type": "string",
                                "format": "numeric",
                                "default": 0
                              },
                              {
                                "type": "number"
                              }
                            ]
                          }
                        ]
                      }
                    ]
                  }
                ]
              }
            ]
          }
        ]
      },
      "path": "/token",
      "message": "Expected required property",
      "errors": [],
      "summary": "Property 'token' is missing"
    },
    {
      "type": 62,
      "schema": {
        "anyOf": [
          {
            "format": "numeric",
            "default": 0,
            "type": "string"
          },
          {
            "type": "number",
            "anyOf": [
              {
                "type": "string",
                "format": "numeric",
                "default": 0
              },
              {
                "type": "number",
                "anyOf": [
                  {
                    "type": "string",
                    "format": "numeric",
                    "default": 0
                  },
                  {
                    "type": "number",
                    "anyOf": [
                      {
                        "type": "string",
                        "format": "numeric",
                        "default": 0
                      },
                      {
                        "type": "number",
                        "anyOf": [
                          {
                            "type": "string",
                            "format": "numeric",
                            "default": 0
                          },
                          {
                            "type": "number",
                            "anyOf": [
                              {
                                "type": "string",
                                "format": "numeric",
                                "default": 0
                              },
                              {
                                "type": "number"
                              }
                            ]
                          }
                        ]
                      }
                    ]
                  }
                ]
              }
            ]
          }
        ]
      },
      "path": "/token",
      "message": "Expected union value",
      "errors": [
        {
          "iterator": {}
        },
        {
          "iterator": {}
        }
      ],
      "summary": "Property 'token' should be one of: 'numeric', 'number'"
    }
  ]
}

Additional information

Either this is a tutorial "bug", or this is an Elysia bug.

Note that this was the easiest way I've found to create a reproducible bug report, but I encountered it on a non-tutorial project first.

Have you try removing the node_modules and bun.lockb and try again yet?

Yes, created a fresh project in repro steps

@Tiv0w Tiv0w added the bug Something isn't working label Mar 27, 2025
@inter0925
Copy link

me too

@ledihildawan
Copy link

ledihildawan commented Apr 2, 2025

The error message appears because the validation optionalSession: t.Optional(t.Ref('session')) in the sign-in process is not working properly. The solution is to remove that line, then change the session type to string, for example session: Record<string, string>. Next, change the key to a string using key.toString().

However, the tutorial page is intended to provide a comprehensive explanation of the framework. Further investigation is needed to understand why the use of typebox optional and references doesn't work properly in this tutorial, or possibly in other applications as well.

The code below will fix the Authentication section in the tutorial by adding validation to check if the user is already logged in and move the sign-out route to the very end without the need for validation, as it is automatically validated by getUserId.

// user.ts

import { Elysia, t } from 'elysia';

export const userService = new Elysia({ name: 'user/service' })
  .state({
    user: {} as Record<string, string>,
    session: {} as Record<string, string>,
  })
  .model({
    signIn: t.Object({
      username: t.String({ minLength: 1 }),
      password: t.String({ minLength: 8 }),
    }),
    session: t.Cookie(
      {
        token: t.String(),
      },
      {
        secrets: 'seia',
      }
    ),
    optionalSession: t.Optional(t.Ref('session')),
  })
  .macro({
    isSignIn(enabled: boolean) {
      if (!enabled) return;

      return {
        beforeHandle({ error, cookie: { token }, store: { session } }) {
          if (!token.value)
            return error(401, {
              success: false,
              message: 'Unauthorized',
            });

          const username = session[token.value as unknown as number];

          if (!username)
            return error(401, {
              success: false,
              message: 'Unauthorized',
            });
        },
      };
    },
  });

export const getUserId = new Elysia()
  .use(userService)
  .guard({
    isSignIn: true,
    cookie: 'session',
  })
  .resolve(({ store: { session }, cookie: { token } }) => ({
    username: session[token.value],
  }))
  .as('plugin');

export const user = new Elysia({ prefix: '/user' })
  .use(userService)
  .put(
    '/sign-up',
    async ({ body: { username, password }, store, error }) => {
      if (store.user[username])
        return error(400, {
          success: false,
          message: 'User already exists',
        });

      store.user[username] = await Bun.password.hash(password);

      return {
        success: true,
        message: 'User created',
      };
    },
    {
      body: 'signIn',
    }
  )
  .post(
    '/sign-in',
    async ({ store: { user, session }, error, body: { username, password }, cookie: { token } }) => {
      if (!user[username] || !(await Bun.password.verify(password, user[username])))
        return error(400, {
          success: false,
          message: 'Invalid username or password',
        });

      if (token.value) {
        return {
          success: true,
          message: 'You are already signed in',
        };
      }

      const key = crypto.getRandomValues(new Uint32Array(1))[0];
      session[key] = username;
      token.value = key.toString();

      return {
        success: true,
        message: `Signed in as ${username}`,
      };
    },
    {
      body: 'signIn',
    }
  )
  .use(getUserId)
  .get('/profile', ({ username }) => ({
    success: true,
    username,
  }))
  .get('/sign-out', ({ cookie: { token } }) => {
    token.remove();

    return {
      success: true,
      message: 'Signed out',
    };
  });

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

3 participants