Pārlūkot izejas kodu

[#1276] mutipart form data

- Add some tests
- Add fixes for preamble, epilogue, transport padding, boundary lines
tim 10 mēneši atpakaļ
vecāks
revīzija
ee8e08cc4a
3 mainītis faili ar 416 papildinājumiem un 128 dzēšanām
  1. 4 2
      src/civetweb.c
  2. 55 23
      src/handle_form.inl
  3. 357 103
      unittest/public_server.c

+ 4 - 2
src/civetweb.c

@@ -10979,7 +10979,7 @@ parse_http_headers(char **buf, struct mg_header hdr[MG_MAX_HEADERS])
 		}
 
 		/* here *dp is either 0 or '\n' */
-		/* in any case, we have a new header */
+		/* in any case, we have found a complete header */
 		num_headers = i + 1;
 
 		if (*dp) {
@@ -10988,9 +10988,11 @@ parse_http_headers(char **buf, struct mg_header hdr[MG_MAX_HEADERS])
 			*buf = dp;
 
 			if ((dp[0] == '\r') || (dp[0] == '\n')) {
-				/* This is the end of the header */
+				/* We've had CRLF twice in a row
+				 * This is the end of the headers */
 				break;
 			}
+			/* continue within the loop, find the next header */
 		} else {
 			*buf = dp;
 			break;

+ 55 - 23
src/handle_form.inl

@@ -162,14 +162,17 @@ search_boundary(const char *buf,
                 const char *boundary,
                 size_t boundary_len)
 {
-	/* We must do a binary search here, not a string search, since the buffer
-	 * may contain '\x00' bytes, if binary data is transferred. */
-	int clen = (int)buf_len - (int)boundary_len - 4;
+	char *boundary_start = "\r\n--";
+	size_t boundary_start_len = strlen(boundary_start);
+
+	/* We must do a binary search here, not a string search, since the
+	 * buffer may contain '\x00' bytes, if binary data is transferred. */
+	int clen = (int)buf_len - (int)boundary_len - boundary_start_len;
 	int i;
 
 	for (i = 0; i <= clen; i++) {
-		if (!memcmp(buf + i, "\r\n--", 4)) {
-			if (!memcmp(buf + i + 4, boundary, boundary_len)) {
+		if (!memcmp(buf + i, boundary_start, boundary_start_len)) {
+			if (!memcmp(buf + i + boundary_start_len, boundary, boundary_len)) {
 				return buf + i;
 			}
 		}
@@ -624,6 +627,7 @@ mg_handle_form_request(struct mg_connection *conn,
 		}
 
 		/* Copy boundary string to variable "boundary" */
+		/* fbeg is pointer to start of value of boundary */
 		fbeg = content_type + bl + 9;
 		bl = strlen(fbeg);
 		boundary = (char *)mg_malloc(bl + 1);
@@ -701,43 +705,71 @@ mg_handle_form_request(struct mg_connection *conn,
 				return -1;
 			}
 
+			/* @see https://www.rfc-editor.org/rfc/rfc2046.html#section-5.1.1
+			 *
+			 * multipart-body := [preamble CRLF]
+			 *     dash-boundary transport-padding CRLF
+			 *     body-part *encapsulation
+			 *     close-delimiter transport-padding
+			 *     [CRLF epilogue]
+			 */
+
 			if (part_no == 0) {
-				int d = 0;
-				while ((d < buf_fill) && (buf[d] != '-')) {
-					d++;
+				size_t preamble_length = 0;
+				/* skip over the preamble until we find a complete boundary */
+				/* +2 for the -- preceding the boundary */
+				while ((preamble_length < buf_fill - bl)
+				       && strncmp(buf + preamble_length + 2, boundary, bl)) {
+					preamble_length++;
 				}
-				if ((d > 0) && (buf[d] == '-')) {
-					memmove(buf, buf + d, (unsigned)buf_fill - (unsigned)d);
-					buf_fill -= d;
+				/* reset the start of buf to remove the preamble */
+				if (0 == strncmp(buf + preamble_length + 2, boundary, bl)) {
+					memmove(buf,
+					        buf + preamble_length,
+					        (unsigned)buf_fill - (unsigned)preamble_length);
+					buf_fill -= preamble_length;
 					buf[buf_fill] = 0;
 				}
 			}
 
-			if (buf[0] != '-' || buf[1] != '-') {
+			/* either it starts with a boundary
+			 * or we couldn't find a boundary at all in the body
+			 * or we didn't have a terminating boundary */
+			if (buf_fill < (bl + 2) || strncmp(buf, "--", 2)
+			    || strncmp(buf + 2, boundary, bl)) {
 				/* Malformed request */
 				mg_free(boundary);
 				return -1;
 			}
-			if (0 != strncmp(buf + 2, boundary, bl)) {
-				/* Malformed request */
-				mg_free(boundary);
-				return -1;
+
+			/* skip the -- */
+			char *boundary_start = buf + 2;
+			size_t transport_padding = 0;
+			while (boundary_start[bl + transport_padding] == ' '
+			       || boundary_start[bl + transport_padding] == '\t') {
+				transport_padding++;
 			}
-			if (buf[bl + 2] != '\r' || buf[bl + 3] != '\n') {
-				/* Every part must end with \r\n, if there is another part.
-				 * The end of the request has an extra -- */
-				if (((size_t)buf_fill != (size_t)(bl + 6))
-				    || (strncmp(buf + bl + 2, "--\r\n", 4))) {
+			char *boundary_end = boundary_start + bl + transport_padding;
+
+			/* after the transport padding, if the boundary isn't
+			 * immediately followed by a \r\n then it is either... */
+			if (strncmp(boundary_end, "\r\n", 2))
+			{
+				/* ...the final boundary, and it is followed by --, (in which
+				 * case it's the end of the request) or it's a malformed
+				 * request */
+				if (strncmp(boundary_end, "--", 2)) {
 					/* Malformed request */
 					mg_free(boundary);
 					return -1;
 				}
-				/* End of the request */
+				/* Ingore any epilogue here */
 				break;
 			}
 
+			/* skip the \r\n */
+			hbuf = boundary_end + 2;
 			/* Next, we need to get the part header: Read until \r\n\r\n */
-			hbuf = buf + bl + 4;
 			hend = strstr(hbuf, "\r\n\r\n");
 			if (!hend) {
 				/* Malformed request */

+ 357 - 103
unittest/public_server.c

@@ -57,6 +57,79 @@
 #define SLEEP_AFTER_MG_STOP (5)
 
 
+#ifdef HEXDUMP
+#else
+void
+hexDumpth(const char *desc, const void *addr, const int len, int perLine)
+{
+	// Silently ignore silly per-line values.
+
+	if (perLine < 4 || perLine > 64)
+		perLine = 16;
+
+	int i;
+	unsigned char buff[perLine + 1];
+	const unsigned char *pc = (const unsigned char *)addr;
+
+	// Output description if given.
+
+	if (desc != NULL)
+		fprintf(stderr, "%s:\n", desc);
+
+	// Length checks.
+
+	if (len == 0) {
+		fprintf(stderr, "  ZERO LENGTH\n");
+		return;
+	}
+	if (len < 0) {
+		fprintf(stderr, "  NEGATIVE LENGTH: %d\n", len);
+		return;
+	}
+
+	// Process every byte in the data.
+
+	for (i = 0; i < len; i++) {
+		// Multiple of perLine means new or first line (with line offset).
+
+		if ((i % perLine) == 0) {
+			// Only print previous-line ASCII buffer for lines beyond first.
+
+			if (i != 0)
+				fprintf(stderr, "  %s\n", buff);
+
+			// Output the offset of current line.
+
+			fprintf(stderr, "  %04x ", i);
+		}
+
+		// Now the hex code for the specific character.
+
+		fprintf(stderr, " %02x", pc[i]);
+
+		// And buffer a printable ASCII character for later.
+
+		if ((pc[i] < 0x20) || (pc[i] > 0x7e)) // isprint() may be better.
+			buff[i % perLine] = '.';
+		else
+			buff[i % perLine] = pc[i];
+		buff[(i % perLine) + 1] = '\0';
+	}
+
+	// Pad out last line if not exactly perLine characters.
+
+	while ((i % perLine) != 0) {
+		fprintf(stderr, "   ");
+		i++;
+	}
+
+	// And print the final ASCII buffer.
+
+	fprintf(stderr, "  %s\n", buff);
+}
+#define HEXDUMP
+#endif
+
 /* Try to communicate with an external http server. */
 static const char *
 get_external_server_ip(void)
@@ -2609,6 +2682,36 @@ FormGet(struct mg_connection *conn, void *cbdata)
 
 
 static int
+FormError(struct mg_connection *conn, void *cbdata)
+{
+	const struct mg_request_info *req_info = mg_get_request_info(conn);
+	int ret;
+	struct mg_form_data_handler fdh = {NULL, NULL, NULL, NULL};
+
+	(void)cbdata;
+
+	ck_assert(req_info != NULL);
+
+	mg_printf(conn, "HTTP/1.0 200 OK\r\nContent-Type: text/plain\r\n\r\n");
+	fdh.user_data = (void *)&g_field_found_return;
+
+	/* Call the form handler */
+	g_field_step = 0;
+	g_field_found_return = MG_FORM_FIELD_STORAGE_GET;
+	ret = mg_handle_form_request(conn, &fdh);
+	g_field_found_return = -888;
+	ck_assert_int_eq(ret, -1);
+	ck_assert_int_eq(g_field_step, 0);
+	mg_printf(conn, "%i\r\n", ret);
+	g_field_step = 1000;
+
+	mark_point();
+
+	return 1;
+}
+
+
+static int
 FormStore(struct mg_connection *conn,
           void *cbdata,
           int ret_expected,
@@ -2727,6 +2830,7 @@ START_TEST(test_handle_form)
 	ck_assert_str_eq(opt, "8884");
 
 	mg_set_request_handler(ctx, "/handle_form", FormGet, NULL);
+	mg_set_request_handler(ctx, "/handle_form_error", FormError, NULL);
 	mg_set_request_handler(ctx, "/handle_form_store", FormStore1, NULL);
 	mg_set_request_handler(ctx, "/handle_form_store2", FormStore2, NULL);
 
@@ -2797,6 +2901,23 @@ START_TEST(test_handle_form)
 	ck_assert_int_eq(client_ri->status_code, 200);
 	mg_close_connection(client_conn);
 
+	/*
+	 * https://datatracker.ietf.org/doc/html/rfc2046#section-5.1
+	 *
+	 * multipart-body := [preamble CRLF]
+	 *     dash-boundary transport-padding CRLF
+	 *     body-part *encapsulation
+	 *     close-delimiter transport-padding
+	 *     [CRLF epilogue]
+	 *
+	 * preamble := discard-text
+	 * epilogue := discard-text
+	 *
+	 * discard-text := *(*text CRLF) *text
+	 *
+	 * text := <any CHAR, including bare CR & bare LF, but NOT including CRLF>
+	 */
+
 	/* Handle form: "POST multipart/form-data" */
 	multipart_body =
 	    "--multipart-form-data-boundary--see-RFC-2388\r\n"
@@ -2922,16 +3043,102 @@ START_TEST(test_handle_form)
 	ck_assert_int_eq(client_ri->status_code, 200);
 	mg_close_connection(client_conn);
 
+
+	/* Handle form: "POST multipart/form-data" with chunked transfer encoding */
+	/* use the most universal possible (no edge cases) body*/
+	client_conn =
+	    mg_download("localhost",
+	                8884,
+	                0,
+	                ebuf,
+	                sizeof(ebuf),
+	                "%s",
+	                "POST /handle_form HTTP/1.1\r\n"
+	                "Host: localhost:8884\r\n"
+	                "Connection: close\r\n"
+	                "Content-Type: multipart/form-data; "
+	                "boundary=multipart-form-data-boundary--see-RFC-2388\r\n"
+	                "Transfer-Encoding: chunked\r\n"
+	                "\r\n");
+
+	ck_assert(client_conn != NULL);
+
+	body_len = strlen(multipart_body);
+	chunk_len = 1;
+	body_sent = 0;
+	while (body_len > body_sent) {
+		if (chunk_len > (body_len - body_sent)) {
+			chunk_len = body_len - body_sent;
+		}
+		ck_assert_int_gt((int)chunk_len, 0);
+		mg_printf(client_conn, "%x\r\n", (unsigned int)chunk_len);
+		mg_write(client_conn, multipart_body + body_sent, chunk_len);
+		mg_printf(client_conn, "\r\n");
+		body_sent += chunk_len;
+		chunk_len = (chunk_len % 40) + 1;
+	}
+	mg_printf(client_conn, "0\r\n\r\n");
+
+	for (sleep_cnt = 0; sleep_cnt < 30; sleep_cnt++) {
+		test_sleep(1);
+		if (g_field_step == 1000) {
+			break;
+		}
+	}
+	client_ri = mg_get_response_info(client_conn);
+
+	ck_assert(client_ri != NULL);
+	ck_assert_int_eq(client_ri->status_code, 200);
+	mg_close_connection(client_conn);
+
+	/* Handle form: "POST multipart/form-data" with chunked transfer
+	 * encoding, using a quoted boundary string */
+	client_conn = mg_download(
+	    "localhost",
+	    8884,
+	    0,
+	    ebuf,
+	    sizeof(ebuf),
+	    "%s",
+	    "POST /handle_form HTTP/1.1\r\n"
+	    "Host: localhost:8884\r\n"
+	    "Connection: close\r\n"
+	    "Content-Type: multipart/form-data; "
+	    "boundary=\"multipart-form-data-boundary--see-RFC-2388\"\r\n"
+	    "Transfer-Encoding: chunked\r\n"
+	    "\r\n");
+
+	ck_assert(client_conn != NULL);
+
+	body_len = strlen(multipart_body);
+	chunk_len = 1;
+	body_sent = 0;
+	while (body_len > body_sent) {
+		if (chunk_len > (body_len - body_sent)) {
+			chunk_len = body_len - body_sent;
+		}
+		ck_assert_int_gt((int)chunk_len, 0);
+		mg_printf(client_conn, "%x\r\n", (unsigned int)chunk_len);
+		mg_write(client_conn, multipart_body + body_sent, chunk_len);
+		mg_printf(client_conn, "\r\n");
+		body_sent += chunk_len;
+		chunk_len = (chunk_len % 40) + 1;
+	}
+	mg_printf(client_conn, "0\r\n\r\n");
+
+	for (sleep_cnt = 0; sleep_cnt < 30; sleep_cnt++) {
+		test_sleep(1);
+		if (g_field_step == 1000) {
+			break;
+		}
+	}
+	client_ri = mg_get_response_info(client_conn);
+
+	ck_assert(client_ri != NULL);
+	ck_assert_int_eq(client_ri->status_code, 200);
+	mg_close_connection(client_conn);
+
 	/* Handle form: "POST multipart/form-data" without trailing CRLF*/
-	/*
-	 * https://datatracker.ietf.org/doc/html/rfc2046#section-5.1
-	 *
-	 * multipart-body := [preamble CRLF]
-	 *     dash-boundary transport-padding CRLF
-	 *     body-part *encapsulation
-	 *     close-delimiter transport-padding
-	 *     [CRLF epilogue]
-	 */
 	multipart_body =
 		"--multipart-form-data-boundary--see-RFC-2388\r\n"
 	    "Content-Disposition: form-data; name=\"textin\"\r\n"
@@ -3057,21 +3264,6 @@ START_TEST(test_handle_form)
 	mg_close_connection(client_conn);
 
 	/* Handle form: "POST multipart/form-data" with epilogue*/
-	/*
-	 * https://datatracker.ietf.org/doc/html/rfc2046#section-5.1
-	 *
-	 * multipart-body := [preamble CRLF]
-	 *     dash-boundary transport-padding CRLF
-	 *     body-part *encapsulation
-	 *     close-delimiter transport-padding
-	 *     [CRLF epilogue]
-	 *
-	 * epilogue := discard-text
-	 *
-	 * discard-text := *(*text CRLF) *text
-	 *
-	 * text := <any CHAR, including bare CR & bare LF, but NOT including CRLF>
-	 */
 	multipart_body =
 	    "--multipart-form-data-boundary--see-RFC-2388\r\n"
 	    "Content-Disposition: form-data; name=\"textin\"\r\n"
@@ -3164,16 +3356,16 @@ START_TEST(test_handle_form)
 	    "\r\n"
 	    "Text area default text.\r\n"
 	    "--multipart-form-data-boundary--see-RFC-2388--\r\n"
-		"epilogue\r\n"
-		"epilogue\r\n"
-		"\r\n"
-		"epilogue\r\n"
-		"\r\n"
-		"\r\n"
-		"\r\n"
-		"epilogue\r\n";
+	    "epilogue\r\n"
+	    "epilogue\r\n"
+	    "\r\n"
+	    "1234567890-=!@£$%^&*()_+[]{};'\\:\"|,./<>?`~§\r\n"
+	    "\r\n"
+	    "\r\n"
+	    "\r\n"
+	    "epilogue\r\n";
 	body_len = strlen(multipart_body);
-	ck_assert_uint_eq(body_len, 2366); /* not required */
+	ck_assert_uint_eq(body_len, 2453); /* not required */
 
 	client_conn =
 	    mg_download("localhost",
@@ -3228,6 +3420,9 @@ START_TEST(test_handle_form)
 	    "\r\n"
 	    "\r\npreamble"
 	    "\r\n"
+	    "1234567890-=!@£$%^&*()_+[]{};'\\:\"|,./<>?`~§\r\n"
+	    "\r\n"
+	    "\r\n\t\t\t   \t\t\t"
 	    "\r\n"
 	    "\r\n"
 	    "--multipart-form-data-boundary--see-RFC-2388\r\n"
@@ -3322,7 +3517,7 @@ START_TEST(test_handle_form)
 	    "Text area default text.\r\n"
 	    "--multipart-form-data-boundary--see-RFC-2388--\r\n";
 	body_len = strlen(multipart_body);
-	ck_assert_uint_eq(body_len, 2366); /* not required */
+	ck_assert_uint_eq(body_len, 2478); /* not required */
 
 	client_conn =
 	    mg_download("localhost",
@@ -3353,21 +3548,7 @@ START_TEST(test_handle_form)
 	ck_assert_int_eq(client_ri->status_code, 200);
 	mg_close_connection(client_conn);
 
-
 	/* Handle form: "POST multipart/form-data" with transport padding*/
-	/*
-	 * https://datatracker.ietf.org/doc/html/rfc2046#section-5.1
-	 *
-	 * multipart-body := [preamble CRLF]
-	 *     dash-boundary transport-padding CRLF
-	 *     body-part *encapsulation
-	 *     close-delimiter transport-padding
-	 *     [CRLF epilogue]
-	 *
-	 * transport-padding := *LWSP-char
-	 *
-	 * LWSP-char := SPACE / HTAB
-	 */
 	multipart_body =
 	    "--multipart-form-data-boundary--see-RFC-2388           \r\n"
 	    "Content-Disposition: form-data; name=\"textin\"\r\n"
@@ -3461,7 +3642,7 @@ START_TEST(test_handle_form)
 	    "Text area default text.\r\n"
 	    "--multipart-form-data-boundary--see-RFC-2388--\r\n";
 	body_len = strlen(multipart_body);
-	ck_assert_uint_eq(body_len, 2366); /* not required */
+	ck_assert_uint_eq(body_len, 2382); /* not required */
 
 	client_conn =
 	    mg_download("localhost",
@@ -3492,40 +3673,125 @@ START_TEST(test_handle_form)
 	ck_assert_int_eq(client_ri->status_code, 200);
 	mg_close_connection(client_conn);
 
-	/* Handle form: "POST multipart/form-data" with chunked transfer encoding */
+	/* Handle form: "POST multipart/form-data" with custom name fields in the
+	 * Content-Disposition */
+	multipart_body =
+	    "--multipart-form-data-boundary--see-RFC-2388\r\n"
+	    "Content-Disposition: form-data; "
+		"custom1name=\"1\"; "
+		"custom2name=\"2\"; "
+		"custom3name=\"3\"; "
+		"custom4name=\"4\"; "
+		"name=\"textin\"\r\n"
+	    "\r\n"
+	    "text\r\n"
+	    "--multipart-form-data-boundary--see-RFC-2388\t\t\t\r\n"
+	    "Content-Disposition: form-data; name=\"passwordin\"\r\n"
+	    "\r\n"
+	    "\r\n"
+	    "--multipart-form-data-boundary--see-RFC-2388\r\n"
+	    "Content-Disposition: form-data; name=\"radio1\"\r\n"
+	    "\r\n"
+	    "val1\r\n"
+	    "--multipart-form-data-boundary--see-RFC-2388\r\n"
+	    "Content-Disposition: form-data; name=radio2\r\n"
+	    "\r\n"
+	    "val1\r\n"
+	    "--multipart-form-data-boundary--see-RFC-2388\r\n"
+	    "Content-Disposition: form-data; name=\"check1\"\r\n"
+	    "\r\n"
+	    "val1\r\n"
+	    "--multipart-form-data-boundary--see-RFC-2388\r\n"
+	    "Content-Disposition: form-data; name=\"numberin\"\r\n"
+	    "\r\n"
+	    "1\r\n"
+	    "--multipart-form-data-boundary--see-RFC-2388\r\n"
+	    "Content-Disposition: form-data; name=\"datein\"\r\n"
+	    "\r\n"
+	    "1.1.2016\r\n"
+	    "--multipart-form-data-boundary--see-RFC-2388\r\n"
+	    "Content-Disposition: form-data; name=\"colorin\"\r\n"
+	    "\r\n"
+	    "#80ff00\r\n"
+	    "--multipart-form-data-boundary--see-RFC-2388\r\n"
+	    "Content-Disposition: form-data; name=\"rangein\"\r\n"
+	    "\r\n"
+	    "3\r\n"
+	    "--multipart-form-data-boundary--see-RFC-2388\r\n"
+	    "Content-Disposition: form-data; name=\"monthin\"\r\n"
+	    "\r\n"
+	    "\r\n"
+	    "--multipart-form-data-boundary--see-RFC-2388\r\n"
+	    "Content-Disposition: form-data; name=\"weekin\"\r\n"
+	    "\r\n"
+	    "\r\n"
+	    "--multipart-form-data-boundary--see-RFC-2388\r\n"
+	    "Content-Disposition: form-data; name=\"timein\"\r\n"
+	    "\r\n"
+	    "\r\n"
+	    "--multipart-form-data-boundary--see-RFC-2388\r\n"
+	    "Content-Disposition: form-data; name=\"datetimen\"\r\n"
+	    "\r\n"
+	    "\r\n"
+	    "--multipart-form-data-boundary--see-RFC-2388\r\n"
+	    "Content-Disposition: form-data; name=\"datetimelocalin\"\r\n"
+	    "\r\n"
+	    "\r\n"
+	    "--multipart-form-data-boundary--see-RFC-2388\r\n"
+	    "Content-Disposition: form-data; name=\"emailin\"\r\n"
+	    "\r\n"
+	    "\r\n"
+	    "--multipart-form-data-boundary--see-RFC-2388\r\n"
+	    "Content-Disposition: form-data; name=\"searchin\"\r\n"
+	    "\r\n"
+	    "\r\n"
+	    "--multipart-form-data-boundary--see-RFC-2388\r\n"
+	    "Content-Disposition: form-data; name=\"telin\"\r\n"
+	    "\r\n"
+	    "\r\n"
+	    "--multipart-form-data-boundary--see-RFC-2388\r\n"
+	    "Content-Disposition: form-data; name=\"urlin\"\r\n"
+	    "\r\n"
+	    "\r\n"
+	    "--multipart-form-data-boundary--see-RFC-2388\r\n"
+	    "Content-Disposition: form-data; name=\"filein\"; filename=\"\"\r\n"
+	    "Content-Type: application/octet-stream\r\n"
+	    "\r\n"
+	    "\r\n"
+	    "--multipart-form-data-boundary--see-RFC-2388\r\n"
+	    "Content-Disposition: form-data; name=filesin; filename=\r\n"
+	    "Content-Type: application/octet-stream\r\n"
+	    "\r\n"
+	    "\r\n"
+	    "--multipart-form-data-boundary--see-RFC-2388\r\n"
+	    "Content-Disposition: form-data; name=\"selectin\"\r\n"
+	    "\r\n"
+	    "opt1\r\n"
+	    "--multipart-form-data-boundary--see-RFC-2388\r\n"
+	    "Content-Disposition: form-data; name=\"message\"\r\n"
+	    "\r\n"
+	    "Text area default text.\r\n"
+	    "--multipart-form-data-boundary--see-RFC-2388--\r\n";
+	body_len = strlen(multipart_body);
+	ck_assert_uint_eq(body_len, 2439); /* not required */
+
 	client_conn =
 	    mg_download("localhost",
 	                8884,
 	                0,
 	                ebuf,
 	                sizeof(ebuf),
-	                "%s",
 	                "POST /handle_form HTTP/1.1\r\n"
 	                "Host: localhost:8884\r\n"
 	                "Connection: close\r\n"
 	                "Content-Type: multipart/form-data; "
 	                "boundary=multipart-form-data-boundary--see-RFC-2388\r\n"
-	                "Transfer-Encoding: chunked\r\n"
-	                "\r\n");
+	                "Content-Length: %u\r\n"
+	                "\r\n%s",
+	                (unsigned int)body_len,
+	                multipart_body);
 
 	ck_assert(client_conn != NULL);
-
-	body_len = strlen(multipart_body);
-	chunk_len = 1;
-	body_sent = 0;
-	while (body_len > body_sent) {
-		if (chunk_len > (body_len - body_sent)) {
-			chunk_len = body_len - body_sent;
-		}
-		ck_assert_int_gt((int)chunk_len, 0);
-		mg_printf(client_conn, "%x\r\n", (unsigned int)chunk_len);
-		mg_write(client_conn, multipart_body + body_sent, chunk_len);
-		mg_printf(client_conn, "\r\n");
-		body_sent += chunk_len;
-		chunk_len = (chunk_len % 40) + 1;
-	}
-	mg_printf(client_conn, "0\r\n\r\n");
-
 	for (sleep_cnt = 0; sleep_cnt < 30; sleep_cnt++) {
 		test_sleep(1);
 		if (g_field_step == 1000) {
@@ -3538,41 +3804,29 @@ START_TEST(test_handle_form)
 	ck_assert_int_eq(client_ri->status_code, 200);
 	mg_close_connection(client_conn);
 
-	/* Handle form: "POST multipart/form-data" with chunked transfer
-	 * encoding, using a quoted boundary string */
-	client_conn = mg_download(
-	    "localhost",
-	    8884,
-	    0,
-	    ebuf,
-	    sizeof(ebuf),
-	    "%s",
-	    "POST /handle_form HTTP/1.1\r\n"
-	    "Host: localhost:8884\r\n"
-	    "Connection: close\r\n"
-	    "Content-Type: multipart/form-data; "
-	    "boundary=\"multipart-form-data-boundary--see-RFC-2388\"\r\n"
-	    "Transfer-Encoding: chunked\r\n"
-	    "\r\n");
-
-	ck_assert(client_conn != NULL);
-
+	/* Handle form error cases */
+	/* Handle form: "POST multipart/form-data" empty body */
+	multipart_body = "";
 	body_len = strlen(multipart_body);
-	chunk_len = 1;
-	body_sent = 0;
-	while (body_len > body_sent) {
-		if (chunk_len > (body_len - body_sent)) {
-			chunk_len = body_len - body_sent;
-		}
-		ck_assert_int_gt((int)chunk_len, 0);
-		mg_printf(client_conn, "%x\r\n", (unsigned int)chunk_len);
-		mg_write(client_conn, multipart_body + body_sent, chunk_len);
-		mg_printf(client_conn, "\r\n");
-		body_sent += chunk_len;
-		chunk_len = (chunk_len % 40) + 1;
-	}
-	mg_printf(client_conn, "0\r\n\r\n");
+	ck_assert_uint_eq(body_len, 0); /* not required */
 
+	client_conn =
+	    mg_download("localhost",
+	                8884,
+	                0,
+	                ebuf,
+	                sizeof(ebuf),
+	                "POST /handle_form_error HTTP/1.1\r\n"
+	                "Host: localhost:8884\r\n"
+	                "Connection: close\r\n"
+	                "Content-Type: multipart/form-data; "
+	                "boundary=multipart-form-data-boundary--see-RFC-2388\r\n"
+	                "Content-Length: %u\r\n"
+	                "\r\n%s",
+	                (unsigned int)body_len,
+	                multipart_body);
+
+	ck_assert(client_conn != NULL);
 	for (sleep_cnt = 0; sleep_cnt < 30; sleep_cnt++) {
 		test_sleep(1);
 		if (g_field_step == 1000) {