参考文献
- https://stackoverflow.com/questions/24535189/composing-multipart-form-data-with-a-different-content-type-on-each-parts-with-j
- https://www.reddit.com/r/lua/comments/yaizxv/lua_post_multipartformdata_and_a_file_via/?rdt=60519
- https://github.com/rstudio/redx/issues/19
client_body_buffer_sizehttps://github.com/apache/apisix/issues/10692client_body_buffer_sizehttps://github.com/apache/apisix/issues/6741
问题产生的原因
后端有个文件上传服务,前端可以直接像文件上传到服务器,但这个上传服务除了有form-data文件流之外,还需要有其它key/value的表单参数,这些参数是固定的,或者有一定的规则,这时我们通过apisix代理一下,就显得更加灵活和理了。
http中的multipart/form-data消息体如下
修改后的请求,是一个标准的http请求,你通过postman的codesnippet视图也可以看到,代码如下
POST /mobile-server/manager/6.0.0.0.0/cdnManage/customUpload HTTP/1.1 Host: api-gw-test.pkulaw.com Cookie: CookieId=b97385476b3c721c81a9163f1c8a85dd; SUB=347c9e9e-076c-45e3-be74-c482fffcc6e5; preferred_username=test; session_state=458053bd-5970-4200-9b6f-cf538ec9808b Content-Length: 508 Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW ----WebKitFormBoundary7MA4YWxkTrZu0gW Content-Disposition: form-data; name="folder" app/icon ----WebKitFormBoundary7MA4YWxkTrZu0gW Content-Disposition: form-data; name="domain" https://static.pkulaw.com ----WebKitFormBoundary7MA4YWxkTrZu0gW Content-Disposition: form-data; name="fileName" xzcf.png ----WebKitFormBoundary7MA4YWxkTrZu0gW Content-Disposition: form-data; name="multipartFile"; filename="/C:/Users/User/Pictures/21111.png" Content-Type: image/png (data) ----WebKitFormBoundary7MA4YWxkTrZu0gW--
开发过程中的一些坑
- 参数拼接错误,form-data的文件流应该是第一个参数
服务端收到的请求体和参数为空
- 后端服务直接报错,原因有以下几个
- 有空的boundary,
- boundary与字段之间没有rn换行
- 将所有n替换为rn,可能会解决上传文件和参数在接收端为空的问题
- http请求头中的boundary是没有开头的两个减号的,这块非常容易出错,例如
ngx.req.set_header("Content-Type", "multipart/form-data; boundary=" .. boundary) - boundary在各字段之前并不相同,需要着重看一下,一般是------开头,看看是否-的数量不同,可能接收端会有下面的错误,表示请求体拼接不正确
Failed to parse multipart servlet request; nested exception is java.io.IOException: org.apache.tomcat.util.http.fileupload.FileUploadException: Stream ended unexpectedly
- 请求报400,大于8K的文件无法上传
- 原因:apisix中的nginx配置中,
client_max_body_size默认设置为0,表示没有限制,不是它的原因 - 原因:
client_body_buffer_size在apisix中没有设置,它会使用默认值,默认为8K,大于8K的文件就会报错,是这个原因 - 解决:修改apisix-helm/values.yaml文件,nginx.configurationSnippet.httpStart节点添加配置
client_body_buffer_size:15m
nginx: # -- Custom configuration snippet. configurationSnippet: main: | httpStart: | client_body_buffer_size 20m; - 原因:apisix中的nginx配置中,
file-upload-proxy文件上传转发插件源码
-- author: zhangzhanling -- 文件上传服务代理 -- 代理前端,与文件上传服务进行通讯 -- 在请求体中,添加统一的参数 local core = require("apisix.core") local uuid = require("resty.jit-uuid") local ngx = require("ngx") -- 定义原数据格式 local schema = { type = "object", properties = { folder = { type = "string", description = "相对目录" }, domain = { type = "string", description = "图片服务的域名" } } } local _M = { version = 0.1, priority = 1009, --数值超大,优先级越高,因为authz-keycloak是2000,它需要在authz-keycloak之后执行,所以把它定为1000,因为咱们也依赖proxy_rewrite插件 name = "file-upload-proxy", schema = schema } local function get_specific_header(ctx, header_name) local headers = core.request.headers(ctx) local value = headers[header_name] if type(value) == "table" then return table.concat(value, ", ") else return value end end -- 辅助函数:查找边界字符串 local function find_boundary(content_type) return content_type:match("boundary=([^;]+)") end function _M.rewrite(conf, ctx) ngx.req.read_body() local body_data = ngx.req.get_body_data() if not body_data then core.log.warn("Failed to read request body.") return 400 end local content_type = ngx.req.get_headers()["content-type"] local boundary = find_boundary(content_type) if not boundary then core.log.warn("No boundary found in content type.") return 400 end local startBoundary = "--" .. boundary local sub_value = get_specific_header(ctx, "sub") local folder = conf.folder if sub_value then folder = folder .. "/" .. sub_value end ---- 构建新的请求体 local new_body = "" local fileExt = ".jpg" local filename = string.match(body_data, 'filename="([^"]+)"') if filename then -- 从filename中提取扩展名 local _, _, ext = string.find(filename, "%.([^.]+)$") if ext then core.log.info("文件扩展名为: " .. ext) fileExt = "." .. ext; end end -- 添加新字段 local new_fields = { { name = "domain", value = conf.domain }, { name = "fileName", value = uuid() .. fileExt }, { name = "folder", value = folder } } ---- 添加新字段 for _, field in ipairs(new_fields) do new_body = new_body .. string.format("rn%srnContent-Disposition: form-data; name="%s"rnrn%s", startBoundary, field.name, field.value) end new_body = new_body .. "rn" .. body_data -- 设置新的请求体 ngx.req.set_body_data(new_body) -- 更新 Content-Type 头 ngx.req.set_header("Content-Type", "multipart/form-data; boundary=" .. boundary) -- 计算并设置 Content-Length local content_length = string.len(new_body) ngx.req.set_header("Content-Length", content_length) -- 日志输出新请求体和内容长度 core.log.warn("boundary:", boundary) core.log.warn("New request body: ", new_body) core.log.warn("Content-Length: ", content_length) end -- 注册插件 return _M