foreword

Recently, during the development process of using next.js, I found that the server-side set-cookie returned to the browser was unsuccessful, so I studied how to deal with it and shared it with you.

operate

How do front-end and back-end cookies pass each other?

1. How does the front end pass cookies to the back end?

When the front-end calls the back-end interface through axios (or fetch), it is brought to the back-end through the cookie attribute of the request header header (the front-end is a cookie in your browser), provided that the same origin is required, for example: the front-end address is: www.baidu.com , the background is :www.baiud.com/api or api.baidu.com , so that you can access the cookies in the browser.

image.png

2. How does the background pass cookies to the front end?

The background is brought to the front-end browser through the set-cookie attribute of the response request header header, and it can be automatically written to the specified domain name.

image.png

image.png

Understand the execution process of next.js

 const pageA = (props) => {
    return <div> this is Page A</div>
}

export async function getServerSideProps(context) {
  
  const res = await axios({ url: "http://www.baidu.com/api/getUserList", data: xxx });
  const data = res?.data;

  return {
    props: {
      data
    }
  }
}

export default pageA;

The above is a very simple next.js page code. It is divided into two parts. The page pageA and the server get the interface data getServerSideProps. When the page is refreshed or the page is opened for the first time, the getServerSideProps method is executed first, and pageA is only after the execution is completed. In the body of the method, is such an execution process.

This is also the core of SSR rendering. It first returns data from the background and then renders the page, reducing the waiting time for SPA to interpret js.

Note: getServerSideProps The method is only executed when the page is rendered for the first time (or refresh the page, jump to the page), and it will not come in later.

How does the next.js client pass the cookie to the background?

First of all, let's look at a problem that the above http://www.baidu.com/api/getUserList interface cannot pass cookies to the background, why can't the cookie be passed to the background? How can I pass the cookie to the background?

Answer the first question first, because getServerSideProps is executed on the server side, so the cookie in the browser cannot be obtained. At this time, it needs to be obtained through the context attribute of next.js.

To answer the second question, it is only manually specified by the code below, which is also what makes SSR special.

 axios.defaults.headers.cookie = context.req.headers.cookie || null

Of course, if the page has been rendered, you don't need to be so troublesome when you access the interface through the page control, because the browser will automatically bring the cookie to the request header of the interface for you: request header cookie.

How does the next.js server pass the cookie to the client browser?

After knowing how to pass the cookie from the client to the server, let's solve how to pass the cookie from the server to the client browser. As mentioned above, the background is set-cookie in the response request header returned by the interface. The attribute is passed over. If it is a SPA, it can be set directly into the cookie, but it is not so simple if we are SSR and next.js, so how do we set it?

To do this in two steps:
1. Set the request header returned by axios
2, getServerSideProps method to set set-cookie again

 const axiosInstance = axios.create({
  baseURL: `http://www.baidu.com/api`,
  withCredentials: true,
});


axiosInstance.interceptors.response.use(function (response) {
    axiosInstance.defaults.headers.setCookie = response.headers['set-cookie']
    return response;
}, function (error) {

  // 超出 2xx 范围的状态码都会触发该函数。
  // 对响应错误做点什么
  return Promise.reject(error);
});

export default axiosInstance;

The above axiosInstance.defaults.headers.setCookie = response.headers['set-cookie'] code is to assign the set-cookie attribute returned by the background to axiosInstance.defaults.headers.setCookie , and then return to the getServerSideProps method, and then return it to the browser at the end , as follows:

 const pageA = (props) => {
    return <div> this is Page A</div>
}

export async function getServerSideProps(context) {

  // 1、获取cookie并保存到axios请求头cookie中
  axios.defaults.headers.cookie = ctx.req.headers.cookie || null
  
  const res = await axios({ url: "http://www.baidu.com/api/getUserList", data: xxx });
  const data = res?.data;

  // 2、判断请求头中是否有set-cookie,如果有,则保存并同步到浏览器中
  if(axios.defaults.headers.setCookie){
    ctx.res.setHeader('set-cookie', axios.defaults.headers.setCookie)
    delete axios.defaults.headers.setCookie
  }
  return {
    props: {
      data
    }
  }
}

export default pageA;

This completes the background set-cookie synchronization cookie to the client cookie, but there is still a problem here, that is, if you request more than one interface in the getServerSideProps method, the set-cookie only starts from the last one. Use, what do you mean?

 const res1 = await axios({ url: "http://www.baidu.com/api/getUserList1", data: xxx });
const res2 = await axios({ url: "http://www.baidu.com/api/getUserList2", data: xxx });
const res3 = await axios({ url: "http://www.baidu.com/api/getUserList3", data: xxx });

After the above three methods are executed, only getUserList3 the set-cookie of this interface is saved to the client cookie. Why? Let's look at this code again:

 axiosInstance.defaults.headers.setCookie = response.headers['set-cookie']

After each execution of the above code axiosInstance.defaults.headers.setCookie will be directly overwritten by response.headers['set-cookie'] , so when the code is executed from getUserList1 to getUserList3 , set-cookie is the last method set-cookie .

Seeing the above problem, have you already thought of it? Yes, it is to merge the three methods set-cookie into axiosInstance.defaults.headers.setCookie , so let's modify the code again:

 // 添加响应拦截器
axiosInstance.interceptors.response.use(function (response) {

  // 目标:合并setCookie
  // A、将response.headers['set-cookie']合并到axios.defaults.headers.setCookie中
  // B、将axios.defaults.headers.setCookie合并到axios.defaults.headers.cookie中,目的是:每次请求axios请求头中的cookie都是最新的

  // 注意:set-cookie格式和cookie格式区别
  /** axios.defaults.headers.setCookie和response.headers['set-cookie']格式如下
   *
   *  axios.defaults.headers.setCookie = [
   *    'name=Justin; Path=/; Max-Age=365; Expires=Mon, 15 Aug 2022 13:35:08 GMT; Secure; HttpOnly; SameSite=None'
   *  ]
   *
   * **/

  /** axios.defaults.headers.cookie 格式如下
   *
   *  axios.defaults.headers.cookie = name=Justin;age=18;sex=男
   *
   * **/
  // A1、判断是否是服务端,并且返回请求头中有set-cookie
  if(typeof window === 'undefined' && response.headers['set-cookie']){
    // A2、判断axios.defaults.headers.setCookie是否是数组
    // A2.1、如果是,则将response.headers['set-cookie']合并到axios.defaults.headers.setCookie
    // 注意:axios.defaults.headers.setCookie默认是undefined,而response.headers['set-cookie']默认是数组
    if(Array.isArray(axiosInstance.defaults.headers.setCookie) ){

      // A2.1.1、将后台返回的set-cookie字符串和axios.defaults.headers.setCookie转化成对象数组
      // 注意:response.headers['set-cookie']可能有多个,它是一个数组

      /** setCookie.parse(response.headers['set-cookie'])和setCookie.parse(axios.defaults.headers.setCookie)格式如下
       *
       setCookie.parse(response.headers['set-cookie']) = [
          {
            name: 'userName',
            value: 'Justin',
            path: '/',
            maxAge: 365,
            expires: 2022-08-16T07:56:46.000Z,
            secure: true,
            httpOnly: true,
            sameSite: 'None'
          }
       ]
       * **/
      const _resSetCookie = setCookie.parse(response.headers['set-cookie'])
      const _axiosSetCookie = setCookie.parse(axiosInstance.defaults.headers.setCookie)
      // A2.1.2、利用reduce,合并_resSetCookie和_axiosSetCookie对象到result中(有则替换,无则新增)
      const result = _resSetCookie.reduce((arr1, arr2)=>{
        // arr1第一次进来是等于初始化化值:_axiosSetCookie
        // arr2依次是_resSetCookie中的对象
        let isFlag = false
        arr1.forEach(item => {
          if(item.name === arr2.name){
            isFlag = true
            item = Object.assign(item, arr2)
          }
        })
        if(!isFlag){
          arr1.push(arr2)
        }
        // 返回结果值arr1,作为reduce下一次的数据
        return arr1
      }, _axiosSetCookie)

      let newSetCookie = []
      result.forEach(item =>{
        // 将cookie对象转换成cookie字符串
        // newSetCookie = ['name=Justin; Path=/; Max-Age=365; Expires=Mon, 15 Aug 2022 13:35:08 GMT; Secure; HttpOnly; SameSite=None']
        newSetCookie.push(cookie.serialize(item.name, item.value, item))
      })
      // A2.1.3、合并完之后,赋值给axios.defaults.headers.setCookie
      axiosInstance.defaults.headers.setCookie = newSetCookie
    }else{
      // A2.2、如果否,则将response.headers['set-cookie']直接赋值
      axiosInstance.defaults.headers.setCookie = response.headers['set-cookie']
    }


    // B1、因为axios.defaults.headers.cookie不是最新的,所以要同步这样后续的请求的cookie都是最新的了
    // B1.1、将axios.defaults.headers.setCookie转化成key:value对象数组
    const _parseSetCookie = setCookie.parse(axiosInstance.defaults.headers.setCookie)
    // B1.2、将axios.defaults.headers.cookie字符串转化成key:value对象
    /** cookie.parse(axiosInstance.defaults.headers.cookie)格式如下
     *
     *  {
     *    userName: Justin,
     *    age: 18,
     *    sex: 男
     *  }
     *
     * **/
    const _parseCookie = cookie.parse(axiosInstance.defaults.headers.cookie)

    // B1.3、将axios.defaults.headers.setCookie赋值给axios.defaults.headers.cookie(有则替换,无则新增)
    _parseSetCookie.forEach(cookie => {
      _parseCookie[cookie.name] = cookie.value
    })
    // B1.4、将赋值后的key:value对象转换成key=value数组
    // 转换成格式为:_resultCookie = ["userName=Justin", "age=19", "sex=男"]
    let _resultCookie = []
    for (const key in _parseCookie) {
      _resultCookie.push(cookie.serialize(key, _parseCookie[key]))
    }
    // B1.5、将key=value的cookie数组转换成key=value;字符串赋值给axiosInstance.defaults.headers.cookie
    // 转换成格式为:axios.defaults.headers.cookie = "userName=Justin;age=19;sex=男"
    axiosInstance.defaults.headers.cookie = _resultCookie.join(';')
  }
  return response;
}, function (error) {
  // 超出 2xx 范围的状态码都会触发该函数。
  // 对响应错误做点什么
  return Promise.reject(error);
});


export default axiosInstance;

a bit much? Don't be afraid, just remove the comments and just a little bit. You can use the comments to see how the above code is implemented. Of course, you can copy it directly. The above code accomplishes two goals:

A. Merge response.headers['set-cookie'] into axios.defaults.headers.setCookie
B. Merge axios.defaults.headers.setCookie into axios.defaults.headers.cookie, the purpose is: the cookie in the request header of each request for axios is the latest

Finally, this completes the delivery of the cookie between the client and the server.

Finally, I would like to say that this situation only occurs when the page is rendered for the first time getServerSideProps The problem encountered in the method, and when the page rendering is completed, there is no need to be so troublesome.

Summarize

1. If you want to know how to solve cross-domain problems, you can read this article
2. The first time the next.js page renders the page, only in the getServerSideProps method, the cookie is obtained and set through the context.


Awbeci
3.1k 声望212 粉丝

Awbeci