skip to Main Content

Basically this other post Express-session does not set cookie? where I’m following Ben Awad’s Fullstack Tutorial. The cookie gets created but the server crashes and this is the error

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'
}

I noticed that this specific line of code in user.ts:

req.session.userId = user.id

when it’s commented out, the error doesn’t occur but the cookie is not set. There isn’t a set-cookie option in the response-header.
My files are pretty much the same as this other person in the post I linked.

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;
};

2

Answers


  1. Chosen as BEST ANSWER

    To be more specific on Bernardo, Ben also changes it to ioredis in the github repo. So you need to install ioredis and add these lines

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

    and delete/comment out the old redisClient lines of code.


  2. I’ve had the same error. In my situation I was able to fix it by changing the redis client to ioredis(I was using redis).

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search