Effect-TS / platform

Unified interfaces for common platform-specific services
https://effect.website
MIT License
41 stars 12 forks source link

How to make a middleware #147

Closed exoer closed 1 year ago

exoer commented 1 year ago

I want to make a rate limiting middleware (ideally targeting specific HTTP routes).

I've looked at the code for the middlewares logging, tracing,... etc.

I've made an attempt, but it is hard to wrap my brain around platform.

import * as Headers from '@effect/platform/Http/Headers';
import type * as Middleware from '@effect/platform/Http/Middleware';
import * as ServerRequest from '@effect/platform/Http/ServerRequest';
import { Effect, pipe } from 'effect';

import { RateLimiterMemory } from 'rate-limiter-flexible';
import { RateLimitError } from './errors';

const rateLimiter = new RateLimiterMemory({
  points: 6,
  duration: 1,
});

export const make = <M extends Middleware.Middleware>(middleware: M): M =>
  middleware;

export const limited = make((httpApp) =>
  Effect.flatMap(ServerRequest.ServerRequest, (request) =>
    Effect.tap(
      httpApp,
      (response) => {
        return Effect.tryPromise({
          try: async () => {            
            return rateLimiter
              .consume(request.headers.ip)
              .then(() => Effect.succeed(response));
          },
          catch: (e) => {
            // want to set status 429 
            return Effect.fail(new RateLimitError());
          },
        });

      }
    )
  )
);

Here is an example of an expess middleware that works with express.


 Example from express middleware
 const rateLimiterMiddleware = (req, res, next) => {
   rateLimiter
     .consume(req.ip)
     .then(() => {
       next();
     })
     .catch(() => {
       res.status(429).send('Too Many Requests');
     });
};

But in platform.Http we don't have access to the response and next object. And it does not look like there is the IP-number in the request.

Thanks for the good work @tim-smart

tim-smart commented 1 year ago

The remote address will be exposed with this PR: https://github.com/Effect-TS/platform/pull/148

Your example can updated to the following:

export const limited = make((httpApp) =>
  Effect.flatMap(ServerRequest.ServerRequest, (request) =>
    Effect.tryPromise(() =>
      rateLimiter.consume(Option.getOrUndefined(request.remoteAddress)),
    ).pipe(
      Effect.match({
        onFailure: () => Effect.succeed(Http.response.empty({ status: 429 })),
        onSuccess: () => httpApp,
      }),
    ),
  ),
)