express-session 设置 cookie 时服务器崩溃?

express-session crashes server on setting cookie?

基本上是另一个 post Express-session does not set cookie?,我正在关注 Ben Awad 的 Fullstack 教程。 cookie 已创建但服务器崩溃,这是错误

node:internal/errors:464
ErrorCaptureStackTrace(err);
^

TypeError [ERR_INVALID_ARG_TYPE]: The "chunk" argument must be of type string or an instance of Buffer or Uint8Array. Received an instance of Array
    at new NodeError (node:internal/errors:371:5)
    at _write (node:internal/streams/writable:312:13)
    at Socket.Writable.write (node:internal/streams/writable:334:10)
    at RedisSocket.writeCommand (/home/kuratar/github/milestone-4-Kuratar/server/node_modules/@node-redis/client/dist/lib/client/socket.js:57:130)
    at Commander._RedisClient_tick (/home/kuratar/github/milestone-4-Kuratar/server/node_modules/@node-redis/client/dist/lib/client/index.js:415:64)
    at Commander._RedisClient_sendCommand (/home/kuratar/github/milestone-4-Kuratar/server/node_modules/@node-redis/client/dist/lib/client/index.js:396:82)
    at Commander.commandsExecutor (/home/kuratar/github/milestone-4-Kuratar/server/node_modules/@node-redis/client/dist/lib/client/index.js:160:154)
    at Commander.BaseClass.<computed> [as set] (/home/kuratar/github/milestone-4-Kuratar/server/node_modules/@node-redis/client/dist/lib/commander.js:8:29)
    at RedisStore.set (/home/kuratar/github/milestone-4-Kuratar/server/node_modules/connect-redis/lib/connect-redis.js:65:21)
    at Session.save (/home/kuratar/github/milestone-4-Kuratar/server/node_modules/express-session/session/session.js:72:25)
    at Session.save (/home/kuratar/github/milestone-4-Kuratar/server/node_modules/express-session/index.js:406:15)
    at ServerResponse.end (/home/kuratar/github/milestone-4-Kuratar/server/node_modules/express-session/index.js:335:21)
    at ServerResponse.send (/home/kuratar/github/milestone-4-Kuratar/server/node_modules/express/lib/response.js:221:10)
    at /home/kuratar/github/milestone-4-Kuratar/server/node_modules/apollo-server-express/dist/ApolloServer.js:89:25 {
  code: 'ERR_INVALID_ARG_TYPE'
}

我注意到 user.ts 中的这一行特定代码:

req.session.userId = user.id

当它被注释掉时,不会出现错误,但不会设置cookie。 response-header 中没有 set-cookie 选项。 我的文件与我链接的 post 中的其他人几乎相同。

index.ts

import "reflect-metadata";
import { MikroORM } from "@mikro-orm/core";
import { __prod__ } from "./constants";
import microConfig from "./mikro-orm.config";
import express from "express";
import { ApolloServer } from "apollo-server-express";
import { ApolloServerPluginLandingPageGraphQLPlayground } from "apollo-server-core";
import { buildSchema } from "type-graphql";
import { HelloResolver } from "./resolvers/hello";
import { PostResolver } from "./resolvers/post";
import { UserResolver } from "./resolvers/user";
import * as redis from "redis";
import session from "express-session";
import connectRedis from "connect-redis";
import { MyContext } from "./types";

// start postgresql server on wsl - sudo service postgresql start
//    stop - sudo service postgresql stop
// start redis server on wsl - redis-server
//    sudo /etc/init.d/redis-server restart
//    stop, start
// watch ts changes - npm run watch
// run server - npm run dev

const main = async () => {
  const orm = await MikroORM.init(microConfig); // initialize database
  await orm.getMigrator().up(); // run migrations before anything else

  const app = express();
  app.set("trust proxy", 1); // trust first proxy

  // this comes before applyMiddleware since use session middleware inside apollo
  const RedisStore = connectRedis(session);
  const redisClient = redis.createClient(); // TODO: TypeError: Cannot read properties of undefined (reading 'createClient')
  redisClient.on("error", (err) => console.log("Redis Client Error", err));
  await redisClient.connect();
  app.use(
    session({
      name: "qid",
      // touch - make request to redis to reset the user's session
      //    if user does something, it means they are active and should reset the timer of automatically logging them out
      //    after 24 hours for example
      // disableTouch: true - keep session forever, can change this later to timed sessions
      store: new RedisStore({ client: redisClient, disableTouch: true }), // tell express session using redis
      cookie: {
        maxAge: 1000 * 60 * 60 * 24 * 365 * 10, // 10 years
        httpOnly: true,
        sameSite: "lax", // csrf
        secure: __prod__, // only works in https
      },
      saveUninitialized: false,
      secret: "askljdhfjkalshdjlf", // want to keep this secret separately
      resave: true,
      rolling: true,
    })
  );

  // app.use(function (req, res, next) {
  //   res.header(
  //     "Access-Control-Allow-Origin",
  //     "https://studio.apollographql.com"
  //   );
  //   res.header("Access-Control-Allow-Credentials", "true");
  //   next();
  // });

  const apolloServer = new ApolloServer({
    schema: await buildSchema({
      resolvers: [HelloResolver, PostResolver, UserResolver],
      validate: false,
    }),
    // object that is accessible by resolvers, basically pass the database itself
    context: ({ req, res }): MyContext => ({ em: orm.em, req, res }),
    plugins: [
      ApolloServerPluginLandingPageGraphQLPlayground({
        settings: { "request.credentials": "include" },
      }),
    ],
  });

  await apolloServer.start();
  const corsOptions = {
    origin: new RegExp("/*/"),
    credentials: true,
  };
  apolloServer.applyMiddleware({ app, cors: corsOptions }); // create graphql endpoint on express
  app.listen(4000, () => {
    console.log("Server started on localhost:4000");
  });
};

main().catch((error) => {
  console.log("----------MAIN CATCHED ERROR----------");
  console.error(error);
  console.log("-----------------END------------------");
});

user.ts

import {
  Resolver,
  Arg,
  Mutation,
  InputType,
  Field,
  Ctx,
  ObjectType,
} from "type-graphql";
import { User } from "../entities/User";
import { MyContext } from "../types";
import argon2 from "argon2";

// another way to implementing arguments for methods instead of @Arg()
@InputType()
class UsernamePasswordInput {
  @Field()
  username: string;
  @Field()
  password: string;
}

@ObjectType()
class FieldError {
  @Field()
  field: string;
  @Field()
  message: string;
}

@ObjectType()
class UserResponse {
  @Field(() => [FieldError], { nullable: true })
  errors?: FieldError[];
  @Field(() => User, { nullable: true })
  user?: User;
}

@Resolver()
export class UserResolver {
  @Mutation(() => UserResponse)
  async register(
    @Arg("options") options: UsernamePasswordInput,
    @Ctx() { em }: MyContext
  ): Promise<UserResponse> {
    if (options.username.length <= 2) {
      return {
        errors: [
          { field: "username", message: "length must be greater than 2" },
        ],
      };
    }
    if (options.password.length <= 2) {
      return {
        errors: [
          { field: "password", message: "length must be greater than 2" },
        ],
      };
    }
    // argon2 is a password hasher package
    const hashedPassword = await argon2.hash(options.password);
    const user = em.create(User, {
      username: options.username,
      password: hashedPassword,
    });
    try {
      await em.persistAndFlush(user);
    } catch (error) {
      // duplicate username error
      if (error.code === "23505") {
        // || error.detail.includes("already exists")
        return {
          errors: [{ field: "username", message: "Username already taken" }],
        };
      }
    }

    // return user in an object since response is now a response object - UserResponse
    return { user };
  }

  @Mutation(() => UserResponse)
  async login(
    @Arg("options") options: UsernamePasswordInput,
    @Ctx() { em, req }: MyContext
  ): Promise<UserResponse> {
    // argon2 is a password hasher package
    const user = await em.findOne(User, {
      username: options.username,
    });
    // can give same field error message like invalid login
    if (!user) {
      return {
        errors: [{ field: "username", message: "That username doesn't exist" }],
      };
    }
    const valid = await argon2.verify(user.password, options.password);
    if (!valid) {
      return {
        errors: [{ field: "password", message: "Incorrect password" }],
      };
    }
    // mutation {
    //   login(options: {username: "eric", password: "eric"}) {
    //     errors {
    //       field
    //       message
    //     }
    //     user {
    //       id
    //       username
    //     }
    //   }
    // }
    console.log(req.session)
    console.log(user.id)
    req.session.userId = user.id
    console.log(req.session)
    console.log(req.session.id)
    // console.log(req.session.userId)

    // return user in an object since response is now a response object - UserResponse
    return { user };
  }
}

types.ts

import { EntityManager, IDatabaseDriver, Connection } from "@mikro-orm/core";
import { Request, Response } from "express";
import { Session, SessionData } from "express-session";

// this is the type of orm.em from index.ts
// extracted to make code look cleaner in post.ts
export type MyContext = {
  em: EntityManager<any> & EntityManager<IDatabaseDriver<Connection>>;
  req: Request & {
    session: Session & Partial<SessionData> & { userId?: number };
  };
  res: Response;
};

我遇到了同样的错误。在我的情况下,我能够通过将 redis 客户端更改为 ioredis(我使用的是 redis)来修复它。

为了更具体地说明 Bernardo,Ben 还在 github 存储库中将其更改为 ioredis。所以你需要安装 ioredis 并添加这些行

import Redis from "ioredis";
const redis = new Redis(process.env.REDIS_URL);

和 delete/comment 删除旧的 redisClient 代码行。