Search code examples
c#dotnet-httpclientform-datacppcms

POSTing file from .NET's HttpClient to CPPCMS returns a 400 Bad Request with no error logging on server


I have a CPPCMS (V1.1.0) server (sample code below) and a C# (.NET 4.0) client (sample code below) and I'm trying to POST a file from the C# client to the CPPCMS server.

The server works if I POST to it from PaleMoon or a CURL (V7.56.1) client (sample code below), so I don't think the problem is in the server code.

The client works if I POST from it to http://posttestserver.com, so I don't think the problem is in the client code.

However, when I try my "working" CPPCMS server against my "working" C# client, I get a 400 Bad Request response. The response is generated by CPPCMS (my first server log is never written as application::main is not called), though CPPCMS does not show or throw any error on the server, even with debug logging enabled.

So I have a client and a server that both work on their own, but not together. Looking at the network trace in Microsoft Network Monitor 3.4 and TCPDUMP, I cannot see what's functionally different between a working request and a failing one. The trace suggests that CPPCMS fails on the very first packet of the POST (of a multi-packet POST request), though this may be a timing issue in the trace, I don't know.

Notes

  • The CPPCMS server is running on Ubuntu 16.04
  • The C# client is running on Windows 7
  • The working CURL test was done from the server machine
  • The working PaleMoon test was done from the client machine
  • I copied the code from Ubuntu into this post by hand, so I may have errors in their samples below

CPPCMS web-server : main.cpp

#include <cppcms/service.h>
#include <cppcms/applications_pool.h>
#include <cppcms/application.h>
#include <cppcms/http_request.h>
#include <iostream>
#include <string>
class Application : public cppcms::application
{  public:
   Application
   (  cppcms::service & service
   ): cppcms::application(service)
   { }
   virtual void main
   (  std::string url
   ){ std::cout
      << "Request for " << url << std::endl
      << "  Uploaded file count "
      << request().files().size()
      << std::endl;
};
int main(int argc, char * * args)
{ cppcms::service service(argc, args);
  service.applications_pool().mount
  ( cppcms::applications_factory<Application>()
  );
  service.run();
  return 0;
}
// g++ -o test main.cpp -lcppcms -lbooster -std=c++11
// ./test -c config.js

CPPCMS web-server : config.js

{ "service":
  { "api": "http"
  , "port": 8080
  , "ip": "10.0.0.4"
  }
, "http": { "script": "/" }
, "logging": { "level": "debug" }
}

C# client : main.cs

using System;
using System.Net.Http;
namespace CPPCMSTest
{  class Program
   {  static void Main(string[] args)
      {  using(var client = new HttpClient())
         using(var content = new MultipartFormDataContent())
         {  content.Add(new StringContent("test"), "name", "filename");
            Console.WriteLine
            (  client
               .  PostAsync("http://10.0.0.4:8080", content)
               .  Result
               .  Content
               .  ReadAsStringAsync()
               .  Result
            );
         }
      }
   }
}
// Output is:
// <html>
// <body>
// <h1>400 Bad Request</h1>
// </body>
// </html>
//
// CPPCMS output is:
// 2018-01-19 12:34:56; cppcms_http, info: POST / (http_api.cpp:249)
//
// That is, CPPCMS doesn't show an error and doesn't reach the application::main

CURL client : main.cpp

#include <curl/curl.h>
#include <iostream>
int main(int argc, char * * args)
{  curl_global_init(CURL_GLOBAL_DEFAULT);
   CURL * curl(curl_easy_init());
   curl_mime * mime(curl_mime_init(curl));
   curl_mime_filedata(curl_mime_addpart(mime), "main.cpp");
   curl_easy_setopt(curl, CURLOPT_MIMEPOST, mime);
   curl_easy_setopt(curl, CURLOPT_URL, "http://10.0.0.4:8080");
   curl_easy_perform(curl);
   curl_mime_free(mime);
   long responseCode;
   curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &responseCode);
   curl_easy_cleanup(curl);
   std::cout << "Response code is " << responseCode << std::endl;
   return 0;
}
// g++ -o test main.cpp -lcurl -std=c++11
// ./test
// Response code is 200
// 
// CPPCMS output is:
// 2018-01-19 12:34:56; cppcms_http, info: POST / (http_api.cpp:249)
// Request for
//   Uploaded file count 1

Solution

  • I've traced the problem back to a bug in CPPCMS. When it parses the headers of an attachment, if the value in the name/value pair is unquoted, it does not correctly position the parser after the value. This causes it to fail because it's expecting a semi-colon or end-of-line, and instead gets the beginning of the previous value. I've submitted a bug report to CPPCMS.

    If the value is quoted, there is no problem, but I can't get C#'s HttpClient to quote all values for Content-Disposition -- it is possible to quote the "name" and "filename" parts (and I've seen another StackOverflow post that shows this as a source of issues), however, HttpClient also adds a "filename*" part, and there seems to be no way of quoting this (as HttpClient replaces literal quotes with %22).

    If anyone else has this problem and needs to update the CPPCMS code themselves, the bug is in private/multipart_parser.h in the parse_pair function. At the end of that function there is an if(*p=='"') ... else ... statement. Under the true condition (the working quoted parser), the block ends with p=tmp;, but under the false condition (the buggy unquoted parser), the block ends with tmp=p;. Updating this to p=tmp; fixes the problem.