You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Delphi 10.3中TWebRequest解析含百分号文件名时异常问题

ISAPI多文件上传含百分号文件名的异常处理方案

问题背景

在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 16:25:03