【问题标题】:File upload fails, when posting with Indy and filename contains Greek characters使用 Indy 发布且文件名包含希腊字符时,文件上传失败
【发布时间】:2017-02-22 12:01:18
【问题描述】:

我正在尝试为 Web 服务实现 POST。我需要发送一个类型可变的文件(.docx.pdf.txt)以及一个 JSON 格式的字符串。

我已成功使用类似于以下的代码发布文件:

procedure DoRequest;
var
  Http: TIdHTTP;
  Params: TIdMultipartFormDataStream;
  RequestStream, ResponseStream: TStringStream;
  JRequest, JResponse: TJSONObject;
  url: string;
begin
  url := 'some_custom_service'

  JRequest := TJSONObject.Create;
  JResponse := TJSONObject.Create;
  try
    JRequest.AddPair('Pair1', 'Value1');
    JRequest.AddPair('Pair2', 'Value2');
    JRequest.AddPair('Pair3', 'Value3');

    Http := TIdHTTP.Create(nil);           
    ResponseStream := TStringStream.Create;
    RequestStream := TStringStream.Create(UTF8Encode(JRequest.ToString));
    try   
      Params := TIdMultipartFormDataStream.Create;
      Params.AddFile('File', ceFileName.Text, '').ContentTransfer := '';
      Params.AddFormField('Json', 'application/json', '', RequestStream);

      Http.Post(url, Params, ResponseStream);
      JResponse := TJSONObject.ParseJSONValue(ResponseStream.DataString) as TJSONObject;
    finally    
      RequestStream.Free;
      ResponseStream.Free;
      Params.Free;
      Http.Free;
    end;
  finally
    JRequest.Free;
    JResponse.Free;
  end;
end;

当我尝试发送文件名中包含希腊字符和空格的文件时出现问题。有时失败,有时成功。

经过大量研究,我注意到 POST 标头是由 Indy 的 TIdFormDataField 类使用 EncodeHeader() 函数编码的。当帖子失败时,标头中的编码文件名会被拆分,而成功的帖子则不会被拆分。

例如:

  • Επιστολή εκπαιδευτικο.docx 编码为=?UTF-8?B?zpXPgM65z4PPhM6/zrvOriDOtc66z4DOsc65zrTOtc+Fz4TOuc66zr8uZG9j?='#$D#$A' =?UTF-8?B?eA==?=,失败。
  • Επιστολή εκπαιδευτικ.docx 被编码为 =?UTF-8?B?zpXPgM65z4PPhM6/zrvOriDOtc66z4DOsc65zrTOtc+Fz4TOuc66LmRvY3g=?=,成功了。
  • Επιστολή εκπαιδευτικ .docx 被编码为 =?UTF-8?B?zpXPgM65z4PPhM6/zrvOriDOtc66z4DOsc65zrTOtc+Fz4TOuc66?= .docx,失败了。

我尝试更改文件名的编码、AddFile() 过程的AContentTypeContentTransfer,但这些都没有改变行为,当编码的文件名被拆分时我仍然会出错.

这是某种错误,还是我遗漏了什么?

我的代码适用于除我上面描述的之外的所有情况。

我正在使用带有 Indy10 的 Delphi XE3。

【问题讨论】:

    标签: delphi utf-8 indy10 delphi-xe3 idhttp


    【解决方案1】:

    EncodeHeader() 确实存在 Unicode 字符串的一些已知问题:

    EncodeHeader() needs to take codeunits into account when splitting data between adjacent encoded-words

    基本上,MIME 编码的单词长度不能超过 75 个字符,因此长文本会被拆分。但是在对长 Unicode 字符串进行编码时,任何给定的 Unicode 字符都可以使用 1 个或更多字节进行字符集编码,并且EncodeHeader() 尚未避免错误地将两个单独字节之间的多字节字符拆分为单独的编码字(这是非法的并被 MIME 规范的RFC 2047 明确禁止)。

    但是,这不是您的示例中发生的情况。

    在您的第一个示例中,'Επιστολή εκπαιδευτικο.docx' 太长而无法编码为单个 MIME 字,因此它被拆分为 'Επιστολή εκπαιδευτικο.doc' 'x' 子字符串,然后分别进行编码。 这在 MIME 中对于长文本是合法的(尽管您可能希望 Indy 将文本拆分为 'Επιστολή' ' εκπαιδευτικο.doc',甚至是 'Επιστολή' ' εκπαιδευτικο' '.doc'。这可能是未来版本中的可能性)。仅由空格分隔的相邻 MIME 单词意味着在解码时连接在一起而不分隔空格,从而再次产生'Επιστολή εκπαιδευτικο.docx'。如果服务器不这样做,则它的解码器存在缺陷(也许它正在解码为'Επιστολή εκπαιδευτικο.doc x'?)。

    在您的第二个示例中,'Επιστολή εκπαιδευτικ.docx' 足够短,可以编码为单个 MIME 字。

    在您的第三个示例中,'Επιστολή εκπαιδευτικ .docx' 在第二个空格(不是第一个空格)上拆分为 'Επιστολή εκπαιδευτικ' ' .docx' 子字符串,并且只需要对第一个子字符串进行编码。 这在 MIME 中是合法的。解码时,解码后的文本将与以下未编码的文本连接,在它们之间保留空格,从而再次生成'Επιστολή εκπαιδευτικ .docx'。如果服务器不这样做,则它的解码器存在缺陷(也许它正在解码为'Επιστολή εκπαιδευτικ.docx'?)。

    如果您通过 Indy 的 MIME 标头编码器/解码器运行这些示例文件名,它们会正确解码:

    var
      s: String;
    begin
      s := EncodeHeader('Επιστολή εκπαιδευτικο.docx', '', 'B', 'UTF-8');
      ShowMessage(s); // '=?UTF-8?B?zpXPgM65z4PPhM6/zrvOriDOtc66z4DOsc65zrTOtc+Fz4TOuc66zr8uZG9j?='#13#10' =?UTF-8?B?eA==?='
      s := DecodeHeader(s);
      ShowMessage(s); // 'Επιστολή εκπαιδευτικο.docx'
    
      s := EncodeHeader('Επιστολή εκπαιδευτικ.docx', '', 'B', 'UTF-8');
      ShowMessage(s); // '=?UTF-8?B?zpXPgM65z4PPhM6/zrvOriDOtc66z4DOsc65zrTOtc+Fz4TOuc66LmRvY3g=?='
      s := DecodeHeader(s);
      ShowMessage(s); // 'Επιστολή εκπαιδευτικ.docx' 
    
      s := EncodeHeader('Επιστολή εκπαιδευτικ .docx', '', 'B', 'UTF-8');
      ShowMessage(s); // '=?UTF-8?B?zpXPgM65z4PPhM6/zrvOriDOtc66z4DOsc65zrTOtc+Fz4TOuc66?= .docx' 
      s := DecodeHeader(s);
      ShowMessage(s); // 'Επιστολή εκπαιδευτικ .docx'
    end;
    

    所以问题似乎出在服务器端解码,而不是 Indy 的客户端编码。

    话虽如此,如果您使用的是相当新的 Indy 10 版本(2011 年 11 月或更高版本),TIdFormDataField 有一个 HeaderEncoding 属性,在 Unicode 环境中默认为 'B' (base64)。但是,拆分逻辑也会影响'Q' (quoted-printable),所以这可能对你有用,也可能对你不起作用(但你可以试试):

    with Params.AddFile('File', ceFileName.Text, '') do
    begin
      ContentTransfer := '';
      HeaderEncoding := 'Q'; // <--- here
      HeaderCharSet := 'utf-8';
    end;
    

    否则,一种解决方法可能是将值更改为 '8'(8 位),从而有效地禁用 MIME 编码(但不是字符集编码):

    with Params.AddFile('File', ceFileName.Text, '') do
    begin
      ContentTransfer := '';
      HeaderEncoding := '8'; // <--- here
      HeaderCharSet := 'utf-8';
    end;
    

    请注意,如果服务器不希望文件名使用原始 UTF-8 字节,您可能仍会遇到问题(例如,'Επιστολή εκπαιδευτικο.docx' 被解释为 'Επιστολή εκπαιδευτικο.docx')。

    【讨论】:

    • 非常感谢@Remy 的回答和所有解释。我已经联系了服务器所有者,我们将尝试一起调试它。与此同时,我尝试了第二种解决方法(8 位),效果很好。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-04-08
    • 2016-07-22
    • 2016-04-22
    • 2018-09-21
    • 2014-10-30
    相关资源
    最近更新 更多