question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Authentication examples assumes all promises a resolved in request order

See original GitHub issue

In the FAQ the recommended approach for managing the identity of the user during the processing of the request is:

app.use(async (ctx, next) => {
  // do whatever checks to determine the user ID
  ctx.state.userId = userId;
  await next();
  delete ctx.state.userId; // cleanup
  });

But according to the documentation context.state is “A record of application state.”. This is also evident in the code in context.ts (L90) where the state in all context instances are the same object as the application state.

 constructor(...) {
    ...
this.state = app.state;

So by default the state object is shared across all request which means if the routes or any middlewares contains asynchronous code where the promises are executed out of order you will not have the state you expect (i.e. a request specific state). You can see this if you implement a route which waits for a promise which is resolved after a second invocation completes together with a middleware which follows the authentication example from the FAQ. For example:

import { Application, Router, Context } from "https://deno.land/x/oak/mod.ts";

export type Continuation = () => Promise<void>;

let resolve: () => void;
let reject;
const promise = new Promise((res, rej) => {
  resolve = res;
  reject = rej;
});

const dump = (ctx: Context) => {
  console.log(
    `The id ${ctx.state.userId} should be equal to ${ctx.request.url.pathname}.`
  );
};

const sayHello = async (ctx: Context) => {
  dump(ctx);

  // do something async, e.g. db, fs, etc.
  if (ctx.request.url.pathname === "/1") {
    await promise;
  }
  dump(ctx);

  if (ctx.request.url.pathname === "/2") {
    resolve();
  }

  ctx.response.body = `Result: ${ctx.state.userId} should be equal to ${ctx.request.url.pathname}.`;
};

// And this is from the Oak  FAQ (https://oakserver.github.io/oak/FAQ).
interface MyState {
  userId: string;
}

const app = new Application<MyState>();

app.use(async (ctx, next) => {
  // do whatever checks to determine the user ID
  // We'll take the path as the userid to prove the point
  ctx.state.userId = ctx.request.url.pathname;
  await next();
  // delete ctx.state.userId;
  ctx.state.userId = "whatever";
});

const router = new Router();
router
  .get("/1", sayHello) //
  .get("/2", sayHello);

app.use(router.routes());

await app.listen({ port: 1993 });

Then executing in 3 separate terminals in order:

deno run --allow-net main.ts

And

curl -X GET http://localhost:1993/1

And

curl -X GET http://localhost:1993/2

Which yields:

The id /1 should be equal to /1.
The id /2 should be equal to /2.
The id /2 should be equal to /2.
The id /2 should be equal to /1. // <--- This is not as intended. 

To achieve a request specific state that is stable across the entire lifetime of the request regardless of resolution order of any promises involved, which is what you need for the authentication pattern to work, you need to introduce a middleware which does something along the lines of:

async (ctx: Context, next: Continuation) => {
  ctx.state = {}; // or new whatever.
  await next();
};

Am I missing something obvious here or are the authentication examples broken since the state object is shared across all requests and therefore cannot be used to store the userid / session id.

Issue Analytics

  • State:open
  • Created 3 years ago
  • Reactions:4
  • Comments:7 (3 by maintainers)

github_iconTop GitHub Comments

1reaction
thesmartcommented, Mar 18, 2021

I have some thoughts on these two points:

Monkey-patching the Context or Request or Response object sure is possible, but I’d love to have nicely typed generics for my request state.

I believe this is already possible through just defining a property to request e.g. ctx.request["user"] = someUser. And I am not really sure how and where defaultRequestState would be useful.

As a user, I’d really appreciate keeping “kernel space” (data provided by Oak) separate from “user space” (data provided by middleware). For example, keeping ctx.request as a pristine (and ideally immutable) representation of the client’s request so that there is no pollution and related unpredictability as a request traverses to down-stack middlewares.

I see ctx.state as an Application-scoped “global” user-memory space. Missing is a per-request user-memory space that ought to be kept separate from the ctx.request. Thus I introduce ctx.locals in #284. Otherwise, there is currently no type-safe and concurrency-safe way for middleware to store data in the stack. The benefit of storing such data in the stack is that it is naturally garbage collected w/o concern of leaking sensitive data between requests.

0reactions
kaleidawavecommented, Oct 29, 2020

Implemented the registerSource suggestion at #250. The benefits are it is a bit stricter than just assigning data in middleware. Being built on getters it will only compute properties when they are called. Currently ambivalent on it and recognise it does not really fit into the whole middleware design.

On the other hand a state property on the Request object would be straightforward to implement + including types so that is probably a better suggestion at this stage. The only thing about it being Partial<RequestState> is that you would have to tell TypeScript that the property exists… but that should be fine.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Implementing Promise.all - by Chris Opperwall - Medium
all is a static method on the Promise object that takes a list of items and returns a promise that resolves with a...
Read more >
Promise API - The Modern JavaScript Tutorial
Promise.all takes an iterable (usually, an array of promises) and returns a new promise. The new promise resolves when all listed promises ......
Read more >
Resolvers - Apollo GraphQL Docs
We want to be able to query the user field to fetch a user by its id . To achieve this, our server...
Read more >
Axios Interceptors retry original request and access original ...
For the incoming direction - if there is an error, I return a promise (and AXIOS will try to resolve it). The promise...
Read more >
@aws-sdk/credential-providers | AWS SDK for JavaScript v3
Supported Configuration; SSO login with AWS CLI; Sample Files ... The ARN of the role to be assumed when multiple roles were received...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found