Delphi 10.3中TWebRequest解析含百分号文件名时异常问题
问题背景
在ISAPI应用中使用TWebRequest接收POST/multipart/form-data类型的多文件上传请求时,若文件名包含百分号(%)(例如Sales % by date.doc),调用request.ContentFields.Count、request.ExtractContentFields()等方法解析请求时会抛出Invalid URL encoded character (% b) at position xx类异常。尝试过拦截修正文件名(如改为Sales %25 by date.doc)但无法重新设置Request.Content解析;客户端浏览器提交前重命名文件受<input type=file>限制无法实现。
一、能否修改文件名的解码方式?
可以通过自定义解析逻辑绕过默认的URL解码限制。TWebRequest的默认解析会把文件名里的%当作URL编码字符处理,而实际文件名中的%是原始字符,并非编码后的内容,因此需要手动解析原始请求内容。
具体实现思路:
- 直接读取
Request.RawContent原始内容,手动拆分multipart/form-data格式,跳过默认的ExtractContentFields方法。 - 解析文件名时,仅当
%后紧跟两位十六进制字符时按URL编码解码,单个%直接保留。
示例代码片段(Delphi):
function ParseMultipartContent(const RawContent, Boundary: string): TStringList; var Parts: TStringList; I: Integer; PartContent, Filename: string; FilenameStart, FilenameEnd: Integer; begin Result := TStringList.Create; Parts := TStringList.Create; try // 按multipart分隔符拆分原始内容 Parts.Delimiter := '--' + Boundary; Parts.StrictDelimiter := True; Parts.DelimitedText := RawContent; // 遍历有效部分(跳过首尾的分隔符边界) for I := 1 to Parts.Count - 2 do begin PartContent := Parts[I]; FilenameStart := Pos('filename="', PartContent); if FilenameStart > 0 then begin FilenameStart := FilenameStart + 10; FilenameEnd := Pos('"', PartContent, FilenameStart); if FilenameEnd > FilenameStart then begin Filename := Copy(PartContent, FilenameStart, FilenameEnd - FilenameStart); // 直接保留原始%,如需编码可替换为%25 Result.Add('Filename: ' + Filename); // 此处可继续提取文件内容等其他字段 end; end; end; finally Parts.Free; end; end;
二、Delphi 11是否修复该问题?
目前官方未在Delphi 11的更新日志中明确标注修复此问题,实际测试中该异常仍会出现——TWebRequest的ExtractContentFields方法依然默认对文件名做严格的URL编码校验,单个%会被判定为无效编码字符。
三、其他解决方案或技巧
1. 服务器端拦截原始请求修正文件名
在调用ExtractContentFields或访问ContentFields之前,拦截原始请求内容,将文件名中的单个%替换为%25(URL编码后的百分号),同时更新Content-Length请求头。
示例代码片段:
procedure FixFilenamePercentSign(var Request: TWebRequest); var RawContent, FixedContent: string; StartPos, EndPos: Integer; begin RawContent := Request.RawContent; FixedContent := RawContent; StartPos := Pos('filename="', FixedContent); while StartPos > 0 do begin StartPos := StartPos + 10; EndPos := Pos('"', FixedContent, StartPos); if EndPos > StartPos then begin // 替换文件名区间内的单个%为%25 FixedContent := Copy(FixedContent, 1, StartPos - 1) + StringReplace(Copy(FixedContent, StartPos, EndPos - StartPos), '%', '%25', [rfReplaceAll]) + Copy(FixedContent, EndPos, Length(FixedContent) - EndPos + 1); end; StartPos := Pos('filename="', FixedContent, EndPos); end; // 更新请求内容和Content-Length头 Request.Content := FixedContent; Request.SetFieldByName('Content-Length', IntToStr(Length(FixedContent))); end;
2. 使用第三方HTTP解析库
替换TWebRequest的默认解析逻辑,采用更宽松的表单解析库(如Indy的TIdMultipartFormDataStream),这类库支持文件名中包含未编码的%,或可配置解码规则。
示例代码片段:
procedure ParseWithIndy(const RawContent, Boundary: string); var Stream: TStringStream; FormData: TIdMultipartFormDataStream; I: Integer; begin Stream := TStringStream.Create(RawContent); FormData := TIdMultipartFormDataStream.Create; try FormData.Boundary := Boundary; FormData.LoadFromStream(Stream); for I := 0 to FormData.Count - 1 do begin if FormData.Items[I].IsFile then begin // 获取处理后的文件名(自动兼容含%的情况) ShowMessage('处理后的文件名: ' + FormData.Items[I].FileName); // 此处可读取文件内容进行后续处理 end; end; finally FormData.Free; Stream.Free; end; end;
内容的提问来源于stack exchange,提问作者AngryViking

