Skip to content

Commit 0aa892b

Browse files
committed
Task 5
1 parent 0cdb159 commit 0aa892b

2 files changed

Lines changed: 359 additions & 0 deletions

File tree

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
using System;
17+
using System.IO;
18+
using System.Net;
19+
using System.Net.Http;
20+
using System.Text;
21+
using System.Threading.Tasks;
22+
23+
namespace Amazon.Lambda.RuntimeSupport
24+
{
25+
/// <summary>
26+
/// HttpContent implementation for streaming responses with chunked transfer encoding.
27+
/// </summary>
28+
internal class StreamingHttpContent : HttpContent
29+
{
30+
private static readonly byte[] CrlfBytes = Encoding.ASCII.GetBytes("\r\n");
31+
private static readonly byte[] FinalChunkBytes = Encoding.ASCII.GetBytes("0\r\n");
32+
33+
private readonly ResponseStream _responseStream;
34+
35+
public StreamingHttpContent(ResponseStream responseStream)
36+
{
37+
_responseStream = responseStream ?? throw new ArgumentNullException(nameof(responseStream));
38+
}
39+
40+
protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context)
41+
{
42+
foreach (var chunk in _responseStream.Chunks)
43+
{
44+
await WriteChunkAsync(stream, chunk);
45+
}
46+
47+
await WriteFinalChunkAsync(stream);
48+
49+
if (_responseStream.HasError)
50+
{
51+
await WriteErrorTrailersAsync(stream, _responseStream.ReportedError);
52+
}
53+
54+
await stream.FlushAsync();
55+
}
56+
57+
protected override bool TryComputeLength(out long length)
58+
{
59+
length = -1;
60+
return false;
61+
}
62+
63+
private async Task WriteChunkAsync(Stream stream, byte[] data)
64+
{
65+
var chunkSizeHex = data.Length.ToString("X");
66+
var chunkSizeBytes = Encoding.ASCII.GetBytes(chunkSizeHex);
67+
68+
await stream.WriteAsync(chunkSizeBytes, 0, chunkSizeBytes.Length);
69+
await stream.WriteAsync(CrlfBytes, 0, CrlfBytes.Length);
70+
await stream.WriteAsync(data, 0, data.Length);
71+
await stream.WriteAsync(CrlfBytes, 0, CrlfBytes.Length);
72+
}
73+
74+
private async Task WriteFinalChunkAsync(Stream stream)
75+
{
76+
await stream.WriteAsync(FinalChunkBytes, 0, FinalChunkBytes.Length);
77+
}
78+
79+
private async Task WriteErrorTrailersAsync(Stream stream, Exception exception)
80+
{
81+
var exceptionInfo = ExceptionInfo.GetExceptionInfo(exception);
82+
83+
var errorTypeHeader = $"{StreamingConstants.ErrorTypeTrailer}: {exceptionInfo.ErrorType}\r\n";
84+
var errorTypeBytes = Encoding.UTF8.GetBytes(errorTypeHeader);
85+
await stream.WriteAsync(errorTypeBytes, 0, errorTypeBytes.Length);
86+
87+
var errorBodyJson = LambdaJsonExceptionWriter.WriteJson(exceptionInfo);
88+
var errorBodyHeader = $"{StreamingConstants.ErrorBodyTrailer}: {errorBodyJson}\r\n";
89+
var errorBodyBytes = Encoding.UTF8.GetBytes(errorBodyHeader);
90+
await stream.WriteAsync(errorBodyBytes, 0, errorBodyBytes.Length);
91+
92+
await stream.WriteAsync(CrlfBytes, 0, CrlfBytes.Length);
93+
}
94+
}
95+
}
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
/*
2+
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
using System;
17+
using System.IO;
18+
using System.Text;
19+
using System.Threading.Tasks;
20+
using Xunit;
21+
22+
namespace Amazon.Lambda.RuntimeSupport.UnitTests
23+
{
24+
public class StreamingHttpContentTests
25+
{
26+
private const long MaxResponseSize = 20 * 1024 * 1024;
27+
28+
private async Task<byte[]> SerializeContentAsync(StreamingHttpContent content)
29+
{
30+
using var ms = new MemoryStream();
31+
await content.CopyToAsync(ms);
32+
return ms.ToArray();
33+
}
34+
35+
// --- Task 5.4: Chunked encoding format tests ---
36+
37+
/// <summary>
38+
/// Property 9: Chunked Encoding Format - chunks are formatted as size(hex) + CRLF + data + CRLF.
39+
/// Validates: Requirements 4.3, 10.1, 10.2
40+
/// </summary>
41+
[Theory]
42+
[InlineData(1)]
43+
[InlineData(10)]
44+
[InlineData(255)]
45+
[InlineData(4096)]
46+
public async Task ChunkedEncoding_SingleChunk_CorrectFormat(int chunkSize)
47+
{
48+
var stream = new ResponseStream(MaxResponseSize);
49+
var data = new byte[chunkSize];
50+
for (int i = 0; i < data.Length; i++) data[i] = (byte)(i % 256);
51+
await stream.WriteAsync(data);
52+
53+
var content = new StreamingHttpContent(stream);
54+
var output = await SerializeContentAsync(content);
55+
var outputStr = Encoding.ASCII.GetString(output);
56+
57+
var expectedSizeHex = chunkSize.ToString("X");
58+
Assert.StartsWith(expectedSizeHex + "\r\n", outputStr);
59+
60+
// Verify chunk data follows the size line
61+
var dataStart = expectedSizeHex.Length + 2; // size + CRLF
62+
for (int i = 0; i < chunkSize; i++)
63+
{
64+
Assert.Equal(data[i], output[dataStart + i]);
65+
}
66+
67+
// Verify CRLF after data
68+
Assert.Equal((byte)'\r', output[dataStart + chunkSize]);
69+
Assert.Equal((byte)'\n', output[dataStart + chunkSize + 1]);
70+
}
71+
72+
/// <summary>
73+
/// Property 9: Chunked Encoding Format - multiple chunks each formatted correctly.
74+
/// Validates: Requirements 4.3, 10.1
75+
/// </summary>
76+
[Fact]
77+
public async Task ChunkedEncoding_MultipleChunks_EachFormattedCorrectly()
78+
{
79+
var stream = new ResponseStream(MaxResponseSize);
80+
await stream.WriteAsync(new byte[] { 0xAA, 0xBB });
81+
await stream.WriteAsync(new byte[] { 0xCC });
82+
83+
var content = new StreamingHttpContent(stream);
84+
var output = await SerializeContentAsync(content);
85+
var outputStr = Encoding.ASCII.GetString(output);
86+
87+
// First chunk: "2\r\n" + 2 bytes + "\r\n"
88+
Assert.StartsWith("2\r\n", outputStr);
89+
Assert.Equal(0xAA, output[3]);
90+
Assert.Equal(0xBB, output[4]);
91+
Assert.Equal((byte)'\r', output[5]);
92+
Assert.Equal((byte)'\n', output[6]);
93+
94+
// Second chunk: "1\r\n" + 1 byte + "\r\n"
95+
Assert.Equal((byte)'1', output[7]);
96+
Assert.Equal((byte)'\r', output[8]);
97+
Assert.Equal((byte)'\n', output[9]);
98+
Assert.Equal(0xCC, output[10]);
99+
Assert.Equal((byte)'\r', output[11]);
100+
Assert.Equal((byte)'\n', output[12]);
101+
}
102+
103+
/// <summary>
104+
/// Property 20: Final Chunk Termination - final chunk "0\r\n" is written.
105+
/// Validates: Requirements 10.2, 10.5
106+
/// </summary>
107+
[Fact]
108+
public async Task FinalChunk_IsWritten()
109+
{
110+
var stream = new ResponseStream(MaxResponseSize);
111+
await stream.WriteAsync(new byte[] { 1 });
112+
113+
var content = new StreamingHttpContent(stream);
114+
var output = await SerializeContentAsync(content);
115+
var outputStr = Encoding.ASCII.GetString(output);
116+
117+
// Output should end with final chunk "0\r\n"
118+
Assert.EndsWith("0\r\n", outputStr);
119+
}
120+
121+
[Fact]
122+
public async Task FinalChunk_EmptyStream_OnlyFinalChunk()
123+
{
124+
var stream = new ResponseStream(MaxResponseSize);
125+
126+
var content = new StreamingHttpContent(stream);
127+
var output = await SerializeContentAsync(content);
128+
129+
Assert.Equal(Encoding.ASCII.GetBytes("0\r\n"), output);
130+
}
131+
132+
/// <summary>
133+
/// Property 22: CRLF Line Terminators - all line terminators are CRLF, not just LF.
134+
/// Validates: Requirements 10.5
135+
/// </summary>
136+
[Fact]
137+
public async Task CrlfTerminators_NoBareLineFeed()
138+
{
139+
var stream = new ResponseStream(MaxResponseSize);
140+
await stream.WriteAsync(new byte[] { 65, 66, 67 }); // "ABC"
141+
142+
var content = new StreamingHttpContent(stream);
143+
var output = await SerializeContentAsync(content);
144+
145+
// Check every \n is preceded by \r
146+
for (int i = 0; i < output.Length; i++)
147+
{
148+
if (output[i] == (byte)'\n')
149+
{
150+
Assert.True(i > 0 && output[i - 1] == (byte)'\r',
151+
$"Found bare LF at position {i} without preceding CR");
152+
}
153+
}
154+
}
155+
156+
[Fact]
157+
public void TryComputeLength_ReturnsFalse()
158+
{
159+
var stream = new ResponseStream(MaxResponseSize);
160+
var content = new StreamingHttpContent(stream);
161+
162+
var result = content.Headers.ContentLength;
163+
164+
Assert.Null(result);
165+
}
166+
167+
// --- Task 5.6: Error trailer tests ---
168+
169+
/// <summary>
170+
/// Property 11: Midstream Error Type Trailer - error type trailer is included for various exception types.
171+
/// Validates: Requirements 5.1, 5.2
172+
/// </summary>
173+
[Theory]
174+
[InlineData(typeof(InvalidOperationException))]
175+
[InlineData(typeof(ArgumentException))]
176+
[InlineData(typeof(NullReferenceException))]
177+
public async Task ErrorTrailer_IncludesErrorType(Type exceptionType)
178+
{
179+
var stream = new ResponseStream(MaxResponseSize);
180+
await stream.WriteAsync(new byte[] { 1 });
181+
var exception = (Exception)Activator.CreateInstance(exceptionType, "test error");
182+
await stream.ReportErrorAsync(exception);
183+
184+
var content = new StreamingHttpContent(stream);
185+
var output = await SerializeContentAsync(content);
186+
var outputStr = Encoding.UTF8.GetString(output);
187+
188+
Assert.Contains($"Lambda-Runtime-Function-Error-Type: {exceptionType.Name}", outputStr);
189+
}
190+
191+
/// <summary>
192+
/// Property 12: Midstream Error Body Trailer - error body trailer includes JSON exception details.
193+
/// Validates: Requirements 5.3
194+
/// </summary>
195+
[Fact]
196+
public async Task ErrorTrailer_IncludesJsonErrorBody()
197+
{
198+
var stream = new ResponseStream(MaxResponseSize);
199+
await stream.WriteAsync(new byte[] { 1 });
200+
await stream.ReportErrorAsync(new InvalidOperationException("something went wrong"));
201+
202+
var content = new StreamingHttpContent(stream);
203+
var output = await SerializeContentAsync(content);
204+
var outputStr = Encoding.UTF8.GetString(output);
205+
206+
Assert.Contains("Lambda-Runtime-Function-Error-Body:", outputStr);
207+
Assert.Contains("something went wrong", outputStr);
208+
Assert.Contains("InvalidOperationException", outputStr);
209+
}
210+
211+
/// <summary>
212+
/// Property 21: Trailer Ordering - trailers appear after final chunk.
213+
/// Validates: Requirements 10.3
214+
/// </summary>
215+
[Fact]
216+
public async Task ErrorTrailers_AppearAfterFinalChunk()
217+
{
218+
var stream = new ResponseStream(MaxResponseSize);
219+
await stream.WriteAsync(new byte[] { 1 });
220+
await stream.ReportErrorAsync(new Exception("fail"));
221+
222+
var content = new StreamingHttpContent(stream);
223+
var output = await SerializeContentAsync(content);
224+
var outputStr = Encoding.UTF8.GetString(output);
225+
226+
var finalChunkIndex = outputStr.IndexOf("0\r\n");
227+
var errorTypeIndex = outputStr.IndexOf("Lambda-Runtime-Function-Error-Type:");
228+
var errorBodyIndex = outputStr.IndexOf("Lambda-Runtime-Function-Error-Body:");
229+
230+
Assert.True(finalChunkIndex >= 0, "Final chunk not found");
231+
Assert.True(errorTypeIndex > finalChunkIndex, "Error type trailer should appear after final chunk");
232+
Assert.True(errorBodyIndex > finalChunkIndex, "Error body trailer should appear after final chunk");
233+
}
234+
235+
[Fact]
236+
public async Task NoError_NoTrailersWritten()
237+
{
238+
var stream = new ResponseStream(MaxResponseSize);
239+
await stream.WriteAsync(new byte[] { 1 });
240+
241+
var content = new StreamingHttpContent(stream);
242+
var output = await SerializeContentAsync(content);
243+
var outputStr = Encoding.UTF8.GetString(output);
244+
245+
Assert.DoesNotContain("Lambda-Runtime-Function-Error-Type:", outputStr);
246+
Assert.DoesNotContain("Lambda-Runtime-Function-Error-Body:", outputStr);
247+
}
248+
249+
[Fact]
250+
public async Task ErrorTrailers_EndWithCrlf()
251+
{
252+
var stream = new ResponseStream(MaxResponseSize);
253+
await stream.WriteAsync(new byte[] { 1 });
254+
await stream.ReportErrorAsync(new Exception("fail"));
255+
256+
var content = new StreamingHttpContent(stream);
257+
var output = await SerializeContentAsync(content);
258+
var outputStr = Encoding.UTF8.GetString(output);
259+
260+
// Should end with final CRLF after trailers
261+
Assert.EndsWith("\r\n", outputStr);
262+
}
263+
}
264+
}

0 commit comments

Comments
 (0)