authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-02-21 23:47:35-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-02-23 02:37:11-07:00
logabde76a808df816ea12a8a2dbf8e6b53ff9b110f
treed817c13501789ae507fc29a069f344eaed28c286
parent380916c0f8883746e4d84d5334f68d0569d76f38

std.http.Server: handle expect: 100-continue requests

The API automatically handles these requests as expected. After receiveHead(), the server has a chance to notice the expectation and do something about it. If it does not, then the Server implementation will handle it by sending the continuation header when the read stream is created. Both respond() and respondStreaming() send the continuation header as part of discarding the request body, only if the read stream has not already been created.

3 files changed, 85 insertions(+), 41 deletions(-)

lib/std/http/Server.zig+69-22
...@@ -313,11 +313,20 @@ pub const Request = struct {...@@ -313,11 +313,20 @@ pub const Request = struct {
313313
314 var first_buffer: [500]u8 = undefined;314 var first_buffer: [500]u8 = undefined;
315 var h = std.ArrayListUnmanaged(u8).initBuffer(&first_buffer);315 var h = std.ArrayListUnmanaged(u8).initBuffer(&first_buffer);
316 if (request.head.expect != null) {
317 // reader() and hence discardBody() above sets expect to null if it
318 // is handled. So the fact that it is not null here means unhandled.
319 h.appendSliceAssumeCapacity("HTTP/1.1 417 Expectation Failed\r\n");
320 if (keep_alive) h.appendSliceAssumeCapacity("connection: keep-alive\r\n");
321 h.appendSliceAssumeCapacity("content-length: 0\r\n\r\n");
322 try request.server.connection.stream.writeAll(h.items);
323 return;
324 }
316 h.fixedWriter().print("{s} {d} {s}\r\n", .{325 h.fixedWriter().print("{s} {d} {s}\r\n", .{
317 @tagName(options.version), @intFromEnum(options.status), phrase,326 @tagName(options.version), @intFromEnum(options.status), phrase,
318 }) catch unreachable;327 }) catch unreachable;
319 if (keep_alive)328
320 h.appendSliceAssumeCapacity("connection: keep-alive\r\n");329 if (keep_alive) h.appendSliceAssumeCapacity("connection: keep-alive\r\n");
321330
322 if (options.transfer_encoding) |transfer_encoding| switch (transfer_encoding) {331 if (options.transfer_encoding) |transfer_encoding| switch (transfer_encoding) {
323 .none => {},332 .none => {},
...@@ -452,25 +461,35 @@ pub const Request = struct {...@@ -452,25 +461,35 @@ pub const Request = struct {
452461
453 var h = std.ArrayListUnmanaged(u8).initBuffer(options.send_buffer);462 var h = std.ArrayListUnmanaged(u8).initBuffer(options.send_buffer);
454463
455 h.fixedWriter().print("{s} {d} {s}\r\n", .{464 const elide_body = if (request.head.expect != null) eb: {
456 @tagName(o.version), @intFromEnum(o.status), phrase,465 // reader() and hence discardBody() above sets expect to null if it
457 }) catch unreachable;466 // is handled. So the fact that it is not null here means unhandled.
458 if (keep_alive) h.appendSliceAssumeCapacity("connection: keep-alive\r\n");467 h.appendSliceAssumeCapacity("HTTP/1.1 417 Expectation Failed\r\n");
468 if (keep_alive) h.appendSliceAssumeCapacity("connection: keep-alive\r\n");
469 h.appendSliceAssumeCapacity("content-length: 0\r\n\r\n");
470 break :eb true;
471 } else eb: {
472 h.fixedWriter().print("{s} {d} {s}\r\n", .{
473 @tagName(o.version), @intFromEnum(o.status), phrase,
474 }) catch unreachable;
475 if (keep_alive) h.appendSliceAssumeCapacity("connection: keep-alive\r\n");
476
477 if (options.content_length) |len| {
478 h.fixedWriter().print("content-length: {d}\r\n", .{len}) catch unreachable;
479 } else {
480 h.appendSliceAssumeCapacity("transfer-encoding: chunked\r\n");
481 }
459482
460 if (options.content_length) |len| {483 for (o.extra_headers) |header| {
461 h.fixedWriter().print("content-length: {d}\r\n", .{len}) catch unreachable;484 h.appendSliceAssumeCapacity(header.name);
462 } else {485 h.appendSliceAssumeCapacity(": ");
463 h.appendSliceAssumeCapacity("transfer-encoding: chunked\r\n");486 h.appendSliceAssumeCapacity(header.value);
464 }487 h.appendSliceAssumeCapacity("\r\n");
488 }
465489
466 for (o.extra_headers) |header| {
467 h.appendSliceAssumeCapacity(header.name);
468 h.appendSliceAssumeCapacity(": ");
469 h.appendSliceAssumeCapacity(header.value);
470 h.appendSliceAssumeCapacity("\r\n");490 h.appendSliceAssumeCapacity("\r\n");
471 }491 break :eb request.head.method == .HEAD;
472492 };
473 h.appendSliceAssumeCapacity("\r\n");
474493
475 return .{494 return .{
476 .stream = request.server.connection.stream,495 .stream = request.server.connection.stream,
...@@ -478,16 +497,20 @@ pub const Request = struct {...@@ -478,16 +497,20 @@ pub const Request = struct {
478 .send_buffer_start = 0,497 .send_buffer_start = 0,
479 .send_buffer_end = h.items.len,498 .send_buffer_end = h.items.len,
480 .content_length = options.content_length,499 .content_length = options.content_length,
481 .elide_body = request.head.method == .HEAD,500 .elide_body = elide_body,
482 .chunk_len = 0,501 .chunk_len = 0,
483 };502 };
484 }503 }
485504
486 pub const ReadError = net.Stream.ReadError || error{ HttpChunkInvalid, HttpHeadersOversize };505 pub const ReadError = net.Stream.ReadError || error{
506 HttpChunkInvalid,
507 HttpHeadersOversize,
508 };
487509
488 fn read_cl(context: *const anyopaque, buffer: []u8) ReadError!usize {510 fn read_cl(context: *const anyopaque, buffer: []u8) ReadError!usize {
489 const request: *Request = @constCast(@alignCast(@ptrCast(context)));511 const request: *Request = @constCast(@alignCast(@ptrCast(context)));
490 const s = request.server;512 const s = request.server;
513
491 const remaining_content_length = &request.reader_state.remaining_content_length;514 const remaining_content_length = &request.reader_state.remaining_content_length;
492 if (remaining_content_length.* == 0) {515 if (remaining_content_length.* == 0) {
493 s.state = .ready;516 s.state = .ready;
...@@ -515,6 +538,7 @@ pub const Request = struct {...@@ -515,6 +538,7 @@ pub const Request = struct {
515 fn read_chunked(context: *const anyopaque, buffer: []u8) ReadError!usize {538 fn read_chunked(context: *const anyopaque, buffer: []u8) ReadError!usize {
516 const request: *Request = @constCast(@alignCast(@ptrCast(context)));539 const request: *Request = @constCast(@alignCast(@ptrCast(context)));
517 const s = request.server;540 const s = request.server;
541
518 const cp = &request.reader_state.chunk_parser;542 const cp = &request.reader_state.chunk_parser;
519 const head_end = request.head_end;543 const head_end = request.head_end;
520544
...@@ -599,11 +623,33 @@ pub const Request = struct {...@@ -599,11 +623,33 @@ pub const Request = struct {
599 return out_end;623 return out_end;
600 }624 }
601625
602 pub fn reader(request: *Request) std.io.AnyReader {626 pub const ReaderError = Response.WriteError || error{
627 /// The client sent an expect HTTP header value other than
628 /// "100-continue".
629 HttpExpectationFailed,
630 };
631
632 /// In the case that the request contains "expect: 100-continue", this
633 /// function writes the continuation header, which means it can fail with a
634 /// write error. After sending the continuation header, it sets the
635 /// request's expect field to `null`.
636 ///
637 /// Asserts that this function is only called once.
638 pub fn reader(request: *Request) ReaderError!std.io.AnyReader {
603 const s = request.server;639 const s = request.server;
604 assert(s.state == .received_head);640 assert(s.state == .received_head);
605 s.state = .receiving_body;641 s.state = .receiving_body;
606 s.next_request_start = request.head_end;642 s.next_request_start = request.head_end;
643
644 if (request.head.expect) |expect| {
645 if (mem.eql(u8, expect, "100-continue")) {
646 try request.server.connection.stream.writeAll("HTTP/1.1 100 Continue\r\n\r\n");
647 request.head.expect = null;
648 } else {
649 return error.HttpExpectationFailed;
650 }
651 }
652
607 switch (request.head.transfer_encoding) {653 switch (request.head.transfer_encoding) {
608 .chunked => {654 .chunked => {
609 request.reader_state = .{ .chunk_parser = http.ChunkParser.init };655 request.reader_state = .{ .chunk_parser = http.ChunkParser.init };
...@@ -639,7 +685,8 @@ pub const Request = struct {...@@ -639,7 +685,8 @@ pub const Request = struct {
639 const s = request.server;685 const s = request.server;
640 if (keep_alive and request.head.keep_alive) switch (s.state) {686 if (keep_alive and request.head.keep_alive) switch (s.state) {
641 .received_head => {687 .received_head => {
642 _ = request.reader().discard() catch return false;688 const r = request.reader() catch return false;
689 _ = r.discard() catch return false;
643 assert(s.state == .ready);690 assert(s.state == .ready);
644 return true;691 return true;
645 },692 },
lib/std/http/test.zig+15-8
...@@ -136,7 +136,7 @@ test "HTTP server handles a chunked transfer coding request" {...@@ -136,7 +136,7 @@ test "HTTP server handles a chunked transfer coding request" {
136 try expect(request.head.transfer_encoding == .chunked);136 try expect(request.head.transfer_encoding == .chunked);
137137
138 var buf: [128]u8 = undefined;138 var buf: [128]u8 = undefined;
139 const n = try request.reader().readAll(&buf);139 const n = try (try request.reader()).readAll(&buf);
140 try expect(std.mem.eql(u8, buf[0..n], "ABCD"));140 try expect(std.mem.eql(u8, buf[0..n], "ABCD"));
141141
142 try request.respond("message from server!\n", .{142 try request.respond("message from server!\n", .{
...@@ -187,13 +187,13 @@ test "echo content server" {...@@ -187,13 +187,13 @@ test "echo content server" {
187187
188 const server_thread = try std.Thread.spawn(.{}, (struct {188 const server_thread = try std.Thread.spawn(.{}, (struct {
189 fn handleRequest(request: *std.http.Server.Request) !void {189 fn handleRequest(request: *std.http.Server.Request) !void {
190 std.debug.print("server received {s} {s} {s}\n", .{190 //std.debug.print("server received {s} {s} {s}\n", .{
191 @tagName(request.head.method),191 // @tagName(request.head.method),
192 @tagName(request.head.version),192 // @tagName(request.head.version),
193 request.head.target,193 // request.head.target,
194 });194 //});
195195
196 const body = try request.reader().readAllAlloc(std.testing.allocator, 8192);196 const body = try (try request.reader()).readAllAlloc(std.testing.allocator, 8192);
197 defer std.testing.allocator.free(body);197 defer std.testing.allocator.free(body);
198198
199 try testing.expect(std.mem.startsWith(u8, request.head.target, "/echo-content"));199 try testing.expect(std.mem.startsWith(u8, request.head.target, "/echo-content"));
...@@ -217,7 +217,7 @@ test "echo content server" {...@@ -217,7 +217,7 @@ test "echo content server" {
217 try w.writeAll("Hello, ");217 try w.writeAll("Hello, ");
218 try w.writeAll("World!\n");218 try w.writeAll("World!\n");
219 try response.end();219 try response.end();
220 std.debug.print(" server finished responding\n", .{});220 //std.debug.print(" server finished responding\n", .{});
221 }221 }
222222
223 fn run(net_server: *std.net.Server) anyerror!void {223 fn run(net_server: *std.net.Server) anyerror!void {
...@@ -237,6 +237,13 @@ test "echo content server" {...@@ -237,6 +237,13 @@ test "echo content server" {
237 if (std.mem.eql(u8, request.head.target, "/end")) {237 if (std.mem.eql(u8, request.head.target, "/end")) {
238 return request.respond("", .{ .keep_alive = false });238 return request.respond("", .{ .keep_alive = false });
239 }239 }
240 if (request.head.expect) |expect| {
241 if (std.mem.eql(u8, expect, "garbage")) {
242 try testing.expectError(error.HttpExpectationFailed, request.reader());
243 try request.respond("", .{ .keep_alive = false });
244 continue;
245 }
246 }
240 handleRequest(&request) catch |err| {247 handleRequest(&request) catch |err| {
241 // This message helps the person troubleshooting determine whether248 // This message helps the person troubleshooting determine whether
242 // output comes from the server thread or the client thread.249 // output comes from the server thread or the client thread.
test/standalone/http.zig+1-11
...@@ -26,17 +26,7 @@ fn handleRequest(request: *http.Server.Request, listen_port: u16) !void {...@@ -26,17 +26,7 @@ fn handleRequest(request: *http.Server.Request, listen_port: u16) !void {
26 request.head.target,26 request.head.target,
27 });27 });
2828
29 if (request.head.expect) |expect| {29 const body = try (try request.reader()).readAllAlloc(salloc, 8192);
30 if (mem.eql(u8, expect, "100-continue")) {
31 @panic("test failure, didn't handle expect 100-continue");
32 } else {
33 return request.respond("", .{
34 .status = .expectation_failed,
35 });
36 }
37 }
38
39 const body = try request.reader().readAllAlloc(salloc, 8192);
40 defer salloc.free(body);30 defer salloc.free(body);
4131
42 var send_buffer: [100]u8 = undefined;32 var send_buffer: [100]u8 = undefined;