1818#include < chrono>
1919#include < queue>
2020#include < filesystem>
21+ #include < random>
22+ #include < sstream>
2123#include < cstring>
2224
2325#ifdef _WIN32
@@ -823,6 +825,7 @@ server_http_res_ptr server_models::proxy_request(const server_http_req & req, co
823825 proxy_path,
824826 req.headers ,
825827 req.body ,
828+ req.files ,
826829 req.should_stop ,
827830 base_params.timeout_read ,
828831 base_params.timeout_write
@@ -1126,6 +1129,77 @@ static bool should_strip_proxy_header(const std::string & header_name) {
11261129 return false ;
11271130}
11281131
1132+ static std::string generate_multipart_boundary () {
1133+ thread_local std::mt19937 gen (std::random_device{}());
1134+ static const char chars[] = " 0123456789abcdefghijklmnopqrstuvwxyz" ;
1135+ std::uniform_int_distribution<> dis (0 , sizeof (chars) - 2 );
1136+ std::string boundary = " ----llama-cpp-proxy-" ;
1137+ for (int i = 0 ; i < 16 ; i++) {
1138+ boundary += chars[dis (gen)];
1139+ }
1140+ return boundary;
1141+ }
1142+
1143+ static std::string build_multipart_body (
1144+ const json & form_fields,
1145+ const std::map<std::string, uploaded_file> & files,
1146+ const std::string & boundary) {
1147+ static auto sanitize_field = [](const std::string & text) {
1148+ std::string result;
1149+ result.reserve (text.size ());
1150+ for (char c : text) {
1151+ if (c != ' \n ' && c != ' \r ' && c != ' "' ) {
1152+ result += c;
1153+ }
1154+ }
1155+ return result;
1156+ };
1157+
1158+ std::ostringstream body;
1159+
1160+ for (const auto & [key, value] : form_fields.items ()) {
1161+ if (value.is_array ()) {
1162+ for (const auto & item : value) {
1163+ body << " --" << boundary << " \r\n " ;
1164+ body << " Content-Disposition: form-data; name=\" " << sanitize_field (key) << " \"\r\n " ;
1165+ body << " \r\n " ;
1166+ if (!item.is_string ()) {
1167+ throw std::invalid_argument (" expected string" );
1168+ }
1169+ body << item.get <std::string>() << " \r\n " ;
1170+ }
1171+ } else {
1172+ body << " --" << boundary << " \r\n " ;
1173+ body << " Content-Disposition: form-data; name=\" " << sanitize_field (key) << " \"\r\n " ;
1174+ body << " \r\n " ;
1175+ if (!value.is_string ()) {
1176+ throw std::invalid_argument (" expected string" );
1177+ }
1178+ body << value.get <std::string>() << " \r\n " ;
1179+ }
1180+ }
1181+
1182+ for (const auto & [key, file] : files) {
1183+ body << " --" << boundary << " \r\n " ;
1184+ body << " Content-Disposition: form-data; name=\" " << sanitize_field (key) << " \" " ;
1185+ if (!file.filename .empty ()) {
1186+ body << " ; filename=\" " << sanitize_field (file.filename ) << " \" " ;
1187+ }
1188+ body << " \r\n " ;
1189+ if (!file.content_type .empty ()) {
1190+ body << " Content-Type: " << sanitize_field (file.content_type ) << " \r\n " ;
1191+ } else {
1192+ body << " Content-Type: application/octet-stream\r\n " ;
1193+ }
1194+ body << " \r\n " ;
1195+ body.write (reinterpret_cast <const char *>(file.data .data ()), file.data .size ());
1196+ body << " \r\n " ;
1197+ }
1198+
1199+ body << " --" << boundary << " --\r\n " ;
1200+ return body.str ();
1201+ }
1202+
11291203server_http_proxy::server_http_proxy (
11301204 const std::string & method,
11311205 const std::string & scheme,
@@ -1134,6 +1208,7 @@ server_http_proxy::server_http_proxy(
11341208 const std::string & path,
11351209 const std::map<std::string, std::string> & headers,
11361210 const std::string & body,
1211+ const std::map<std::string, uploaded_file> & files,
11371212 const std::function<bool ()> should_stop,
11381213 int32_t timeout_read,
11391214 int32_t timeout_write
@@ -1195,28 +1270,65 @@ server_http_proxy::server_http_proxy(
11951270 return pipe->write ({{}, 0 , std::string (data, data_length), " " });
11961271 };
11971272
1273+ // when files are present, the body was converted from multipart form data to JSON
1274+ // we need to reconstruct the multipart body for the downstream server
1275+ std::string effective_body = body;
1276+ std::string override_content_type;
1277+ bool has_files = !files.empty ();
1278+
1279+ if (has_files) {
1280+ json form_fields = json::parse (body, nullptr , false );
1281+ if (!form_fields.is_discarded ()) {
1282+ auto boundary = generate_multipart_boundary ();
1283+ effective_body = build_multipart_body (form_fields, files, boundary);
1284+ override_content_type = " multipart/form-data; boundary=" + boundary;
1285+ } else {
1286+ throw std::runtime_error (" failed to parse multipart form fields JSON" );
1287+ }
1288+ }
1289+
11981290 // prepare the request to destination server
11991291 httplib::Request req;
12001292 {
12011293 req.method = method;
12021294 req.path = path;
12031295 for (const auto & [key, value] : headers) {
1204- if (key == " Accept-Encoding" ) {
1296+ const auto lowered = to_lower_copy (key);
1297+ if (lowered == " accept-encoding" ) {
12051298 // disable Accept-Encoding to avoid compressed responses
12061299 continue ;
12071300 }
1208- if (key == " Transfer-Encoding " ) {
1301+ if (lowered == " transfer-encoding " ) {
12091302 // the body is already decoded
12101303 continue ;
12111304 }
1212- if (key == " Host" || key == " host" ) {
1305+ if (lowered == " content-length" ) {
1306+ // let httplib calculate Content-Length from the actual body
1307+ continue ;
1308+ }
1309+ if (lowered == " content-type" ) {
1310+ if (has_files) {
1311+ // we set our own Content-Type with the new boundary
1312+ continue ;
1313+ }
1314+ // when no files but the original request was multipart,
1315+ // the body is now JSON, so correct the Content-Type
1316+ if (value.find (" multipart/form-data" ) != std::string::npos) {
1317+ override_content_type = " application/json; charset=utf-8" ;
1318+ continue ;
1319+ }
1320+ }
1321+ if (lowered == " host" ) {
12131322 bool is_default_port = (scheme == " https" && port == 443 ) || (scheme == " http" && port == 80 );
12141323 req.set_header (key, is_default_port ? host : host + " :" + std::to_string (port));
12151324 } else {
12161325 req.set_header (key, value);
12171326 }
12181327 }
1219- req.body = body;
1328+ req.body = effective_body;
1329+ if (!override_content_type.empty ()) {
1330+ req.set_header (" Content-Type" , override_content_type);
1331+ }
12201332 req.response_handler = response_handler;
12211333 req.content_receiver = content_receiver;
12221334 }
0 commit comments