Http411请求错误

Posted by Run-dream Blog on June 27, 2019

HTTP 411 请求错误

背景

  1. 服务端有一段将用户提交的建议反馈转发给第三方系统的代码,建议反馈中包含图片。
  2. 测试服上可以正常工作,正式服上某层Nginx代理上返回了411 Length Required。

最小可复现代码

'use strict'

import FormStream from 'formstream'
import urllib from 'urllib'
import {
    basename
} from 'path'

async function request(){
    const formData = FormStream()
    const filepaths = ['a.png', 'b.png']
    for (const filepath of filepaths) {
        formData.file(basename(filepath), filepath)
    }
    
    const params = {
        a: 'xxx',
        b: 'xxx',
    }
    for (const key of Object.keys(params)) {
        formData.field(key, params[key])
    }

    const url = 'http://127.0.0.1:7001/api/test'
    const options = {
        method: 'post',
        dataType: 'json',
        stream: formData,
        headers: formData.headers()
    }
    return urllib.curl(url, options)
}

原因分析

nginx 日志记录发现Content-Length的长度不正确 查看formstream的源代码,只有在_isAllStreamSizeKnown的时候才会向headers里添加Content-Length

FormStream.prototype.headers = function (options) {
  var headers = {
    'Content-Type': 'multipart/form-data; boundary=' + this._boundary
  };

  // calculate total stream size
  this._contentLength += this._knownStreamSize;

  // calculate length of end padding
  this._contentLength += this._endData.length;

  if (this._isAllStreamSizeKnown) {
    headers['Content-Length'] = String(this._contentLength);
  }

  if (options) {
    for (var k in options) {
      headers[k] = options[k];
    }
  }

  return headers;
};

修改后的代码

做以下修改

    // ...

    for (const filepath of filepaths) {
        const fileStat = await promisify(fstat)(filepath)
        formData.file(basename(filepath), filepath, fileStat.size)
    }

    // ...

结果这个请求一直没有发送出去,请求超时

原因分析

对着源码分析了一遍没有找出问题,google后发现了大佬的文章 源码中每次执行完添加数据的代码后都有这么一段

 process.nextTick(this.resume.bind(this));

对于resume,有

 // ...
 FormStream.prototype.drain = function () {
  this._emitBuffers();

  var item = this._streams.shift();
  if (item) {
    this._emitStream(item);
  } else {
    this._emitEnd();
  }

  return this;
};

FormStream.prototype.resume = function () {
  this.paused = false;

  if (!this._draining) {
    this._draining = true;
    this.drain();
  }

  return this;
};

结论

formstream在调用field之类的函数后会注册一个微任务 微任务执行时会使用流开始发送数据,数据发送完毕后关闭流 因为在调用urllib之前还注册了一个微任务,导致urllib.request实际上是在这个微任务内部执行的 也就是说在request执行的时候,流已经关闭了,一直拿不到数据,所以就抛出异常,提示接口超时。

正常工作的代码

'use strict'

import FormStream from 'formstream'
import urllib from 'urllib'
import {
    basename
} from 'path'
import { fstat } from 'fs'
import { promisify } from 'util'


async function request(){
    const filepaths = ['a.png', 'b.png']
    
    const fileSizes = {}
    for(const filepath of filepaths){
        const fileStat = await promisify(fstat)(filepath)
        fileSizes[filepath] = fileStat.size
    }

    const formData = FormStream()
    for (const filepath of filepaths) {
        formData.file(basename(filepath), filepath, fileSizes[filepath])
    }

    const params = {
        a: 'xxx',
        b: 'xxx',
    }
    for (const key of Object.keys(params)) {
        formData.field(key, params[key])
    }

    const url = 'http://127.0.0.1:7001/api/test'
    const options = {
        method: 'post',
        dataType: 'json',
        stream: formData,
        headers: formData.headers()
    }
    return urllib.curl(url, options)
}