diff --git a/src/SpeakEasy.IntegrationTests/BasicAsyncHttpMethods.cs b/src/SpeakEasy.IntegrationTests/BasicAsyncHttpMethods.cs index 632571b..c176cb4 100644 --- a/src/SpeakEasy.IntegrationTests/BasicAsyncHttpMethods.cs +++ b/src/SpeakEasy.IntegrationTests/BasicAsyncHttpMethods.cs @@ -20,10 +20,11 @@ public BasicAsyncHttpMethods(ApiFixture fixture) [Fact] public async void ShouldGetAsync() { - var response = await client.Get("products/1"); - - Assert.Contains(":1337/api/products/1", response.State.RequestUrl.ToString()); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + using (var response = await client.Get("products/1")) + { + Assert.Contains(":1337/api/products/1", response.State.RequestUrl.ToString()); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } } [Fact] @@ -248,7 +249,7 @@ public async void ShouldCallbackWithState() var message = string.Empty; await client.Post("locations") - .On(HttpStatusCode.BadRequest, status => { message = status.StatusDescription; }); + .On(HttpStatusCode.BadRequest, status => { message = status.ReasonPhrase; }); Assert.Equal("titles cannot start with 'bad'", message); } diff --git a/src/SpeakEasy.Specifications/Fixtures/HttpResponses.cs b/src/SpeakEasy.Specifications/Fixtures/HttpResponses.cs index c323906..e058ba4 100644 --- a/src/SpeakEasy.Specifications/Fixtures/HttpResponses.cs +++ b/src/SpeakEasy.Specifications/Fixtures/HttpResponses.cs @@ -1,6 +1,6 @@ -using System; -using System.IO; +using System.IO; using System.Net; +using System.Net.Http; namespace SpeakEasy.Specifications.Fixtures { @@ -16,13 +16,10 @@ public static HttpResponse Create(ISerializer serializer, Stream bodyStream, Htt return new HttpResponse( serializer, bodyStream, - new HttpResponseState(code, - "status description", - new Uri("http://example.com/companies"), + new HttpResponseState( + new HttpResponseMessage { StatusCode = code }, cookies, - "contentType", - "server", - null), null); + "contentType"), null); } } } diff --git a/src/SpeakEasy.Specifications/HttpResponseHandlerSpecs.cs b/src/SpeakEasy.Specifications/HttpResponseHandlerSpecs.cs index e7f3b18..cc62056 100644 --- a/src/SpeakEasy.Specifications/HttpResponseHandlerSpecs.cs +++ b/src/SpeakEasy.Specifications/HttpResponseHandlerSpecs.cs @@ -6,35 +6,28 @@ namespace SpeakEasy.Specifications { [Subject(typeof(HttpResponseHandler))] - class HttpResponseHandlerSpecs : WithSubject + class HttpResponseHandlerSpecs : WithFakes { - class when_unwrapping_as - { - static ISerializer deserializer; - - Establish context = () => - { - deserializer = An(); + static HttpResponseHandler handler; - The().WhenToldTo(r => r.Deserializer).Return(deserializer); - }; + Establish context = () => + handler = new HttpResponseHandler(The(), The(), new SingleUseStream(new MemoryStream(Encoding.UTF8.GetBytes("abcd")))); + class when_unwrapping_as + { Because of = () => - Subject.As(); + handler.As(); It should_deserialize_with_deserializer = () => - deserializer.WasToldTo(d => d.Deserialize(Param.IsAny())); + The().WasToldTo(d => d.Deserialize(Param.IsAny())); } class when_getting_as_byte_array { static byte[] bytes; - Establish context = () => - The().WhenToldTo(r => r.Body).Return(new MemoryStream(Encoding.UTF8.GetBytes("abcd"))); - Because of = () => - bytes = Subject.AsByteArray().Await(); + bytes = handler.AsByteArray().Await(); It should_get_bytes = () => bytes.Length.ShouldBeGreaterThan(0); diff --git a/src/SpeakEasy.Specifications/RequestRunnerSpecs.cs b/src/SpeakEasy.Specifications/RequestRunnerSpecs.cs index 72a53d6..a28e870 100644 --- a/src/SpeakEasy.Specifications/RequestRunnerSpecs.cs +++ b/src/SpeakEasy.Specifications/RequestRunnerSpecs.cs @@ -7,8 +7,6 @@ namespace SpeakEasy.Specifications [Subject(typeof(RequestRunner))] class RequestRunnerSpecs : WithSubject { - static HttpRequestMessage webRequest; - static IHttpRequest request; Establish context = () => diff --git a/src/SpeakEasy/HttpResponse.cs b/src/SpeakEasy/HttpResponse.cs index a7d2fa1..aa53fbe 100644 --- a/src/SpeakEasy/HttpResponse.cs +++ b/src/SpeakEasy/HttpResponse.cs @@ -5,16 +5,21 @@ namespace SpeakEasy { - public class HttpResponse : IHttpResponseWithBody + public class HttpResponse : IHttpResponse { + private readonly ISerializer deserializer; + + private readonly SingleUseStream body; + public HttpResponse( ISerializer deserializer, Stream body, IHttpResponseState state, HttpContentHeaders headers) { - Deserializer = deserializer; - Body = body; + this.deserializer = deserializer; + this.body = new SingleUseStream(body); + State = state; Headers = headers; } @@ -23,10 +28,6 @@ public HttpResponse( public IHttpResponseState State { get; } - public Stream Body { get; } - - public ISerializer Deserializer { get; } - public string ContentType => State.ContentType; public HttpStatusCode StatusCode => State.StatusCode; @@ -53,7 +54,7 @@ public IHttpResponse On(HttpStatusCode code, Action action) return this; } - var deserialied = Deserializer.Deserialize(Body); + var deserialied = deserializer.Deserialize(body.GetAndConsumeStream()); action(deserialied); return this; @@ -86,7 +87,7 @@ public IHttpResponseHandler On(HttpStatusCode code) OnIncorrectStatusCode(code); } - return new HttpResponseHandler(this); + return new HttpResponseHandler(this, deserializer, body); } public IHttpResponseHandler On(int code) @@ -101,7 +102,7 @@ public IHttpResponseHandler OnOk() OnIncorrectStatusCode(HttpStatusCode.OK); } - return new HttpResponseHandler(this); + return new HttpResponseHandler(this, deserializer, body); } private void OnIncorrectStatusCode(HttpStatusCode expected) @@ -133,5 +134,11 @@ public bool IsOk() { return Is(HttpStatusCode.OK); } + + public void Dispose() + { + body.Dispose(); + State.Dispose(); + } } } diff --git a/src/SpeakEasy/HttpResponseHandler.cs b/src/SpeakEasy/HttpResponseHandler.cs index 14255c0..df26c1f 100644 --- a/src/SpeakEasy/HttpResponseHandler.cs +++ b/src/SpeakEasy/HttpResponseHandler.cs @@ -6,27 +6,29 @@ namespace SpeakEasy { internal class HttpResponseHandler : IHttpResponseHandler { - private readonly IHttpResponseWithBody response; + private readonly IHttpResponse response; - public HttpResponseHandler(IHttpResponseWithBody response) + private readonly ISerializer serializer; + + private readonly SingleUseStream body; + + public HttpResponseHandler(IHttpResponse response, ISerializer serializer, SingleUseStream body) { this.response = response; + this.serializer = serializer; + this.body = body; } public IHttpResponse Response => response; public object As(Type type) { - var deserializer = response.Deserializer; - - return deserializer.Deserialize(response.Body, type); + return serializer.Deserialize(body.GetAndConsumeStream(), type); } public T As() { - var deserializer = response.Deserializer; - - return deserializer.Deserialize(response.Body); + return serializer.Deserialize(body.GetAndConsumeStream()); } public T As(Func constructor) @@ -41,18 +43,16 @@ public Task AsByteArray() public async Task AsByteArray(int bufferSize) { - var body = response.Body; - using (var copy = new MemoryStream()) { - await body.CopyToAsync(copy, bufferSize).ConfigureAwait(false); + await body.GetAndConsumeStream().CopyToAsync(copy, bufferSize).ConfigureAwait(false); return copy.ToArray(); } } public async Task AsString() { - using (var reader = new StreamReader(response.Body)) + using (var reader = new StreamReader(body.GetAndConsumeStream())) { return await reader.ReadToEndAsync().ConfigureAwait(false); } @@ -66,7 +66,7 @@ public IFile AsFile() contentDisposition.Name, contentDisposition.FileName, response.ContentType, - response.Body); + body.GetAndConsumeStream()); } } } diff --git a/src/SpeakEasy/HttpResponseState.cs b/src/SpeakEasy/HttpResponseState.cs index 7df5f62..be44bb2 100644 --- a/src/SpeakEasy/HttpResponseState.cs +++ b/src/SpeakEasy/HttpResponseState.cs @@ -1,6 +1,6 @@ using System; using System.Net; -using System.Net.Http.Headers; +using System.Net.Http; namespace SpeakEasy { @@ -9,40 +9,37 @@ namespace SpeakEasy /// public class HttpResponseState : IHttpResponseState { - private readonly HttpContentHeaders headers; + private readonly HttpResponseMessage httpResponseMessage; public HttpResponseState( - HttpStatusCode statusCode, - string statusDescription, - Uri requestUrl, + HttpResponseMessage httpResponseMessage, Cookie[] cookies, - string contentType, - string server, - HttpContentHeaders headers) + string contentType) { - this.headers = headers; - StatusCode = statusCode; - StatusDescription = statusDescription; - RequestUrl = requestUrl; + this.httpResponseMessage = httpResponseMessage; Cookies = cookies; ContentType = contentType; - Server = server; } - public string Server { get; } + public string Server => httpResponseMessage.Headers.Server.ToString(); - public string ContentEncoding => headers.ContentEncoding.ToString(); + public string ContentEncoding => httpResponseMessage.Content.Headers.ContentEncoding.ToString(); - public DateTime LastModified => headers.LastModified.GetValueOrDefault(DateTime.UtcNow).Date; + public DateTime LastModified => httpResponseMessage.Content.Headers.LastModified.GetValueOrDefault(DateTime.UtcNow).Date; - public HttpStatusCode StatusCode { get; } + public HttpStatusCode StatusCode => httpResponseMessage.StatusCode; - public string StatusDescription { get; } + public string ReasonPhrase => httpResponseMessage.ReasonPhrase; - public Uri RequestUrl { get; } + public Uri RequestUrl => httpResponseMessage.RequestMessage.RequestUri; public Cookie[] Cookies { get; } public string ContentType { get; } + + public void Dispose() + { + httpResponseMessage.Dispose(); + } } } diff --git a/src/SpeakEasy/IHttpResponse.cs b/src/SpeakEasy/IHttpResponse.cs index 6a7909c..7c910e2 100644 --- a/src/SpeakEasy/IHttpResponse.cs +++ b/src/SpeakEasy/IHttpResponse.cs @@ -8,7 +8,7 @@ namespace SpeakEasy /// A chainable http response which gives you access to all the data available /// on a response to an http service /// - public interface IHttpResponse + public interface IHttpResponse : IDisposable { HttpContentHeaders Headers { get; } diff --git a/src/SpeakEasy/IHttpResponseState.cs b/src/SpeakEasy/IHttpResponseState.cs index 2a104d1..411e664 100644 --- a/src/SpeakEasy/IHttpResponseState.cs +++ b/src/SpeakEasy/IHttpResponseState.cs @@ -6,20 +6,18 @@ namespace SpeakEasy /// /// An IHttpResponseState contains all the response state from an http endpoint. /// - public interface IHttpResponseState + public interface IHttpResponseState : IDisposable { Uri RequestUrl { get; } - string Server { get; } - string ContentType { get; } - string ContentEncoding { get; } + HttpStatusCode StatusCode { get; } - string StatusDescription { get; } + string ReasonPhrase { get; } - DateTime LastModified { get; } + string ContentEncoding { get; } - HttpStatusCode StatusCode { get; } + DateTime LastModified { get; } } } diff --git a/src/SpeakEasy/IHttpResponseWithBody.cs b/src/SpeakEasy/IHttpResponseWithBody.cs deleted file mode 100644 index 005e6de..0000000 --- a/src/SpeakEasy/IHttpResponseWithBody.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.IO; - -namespace SpeakEasy -{ - internal interface IHttpResponseWithBody : IHttpResponse - { - Stream Body { get; } - - ISerializer Deserializer { get; } - } -} diff --git a/src/SpeakEasy/Middleware/RequestMiddleware.cs b/src/SpeakEasy/Middleware/RequestMiddleware.cs index ec632e6..c59eca6 100644 --- a/src/SpeakEasy/Middleware/RequestMiddleware.cs +++ b/src/SpeakEasy/Middleware/RequestMiddleware.cs @@ -50,12 +50,14 @@ public async Task Invoke(IHttpRequest request, CancellationToken await serializedBody.WriteTo(httpRequest, cancellationToken).ConfigureAwait(false); var httpResponse = await client.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false); - var responseStream = await httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); - - return CreateHttpResponse( - httpRequest, - httpResponse, - responseStream); + { + var responseStream = await httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); + + return CreateHttpResponse( + httpRequest, + httpResponse, + responseStream); + } } } @@ -106,13 +108,9 @@ public IHttpResponse CreateHttpResponse(HttpRequestMessage httpRequest, HttpResp : cookieCollection.Cast().ToArray(); var state = new HttpResponseState( - httpResponse.StatusCode, - httpResponse.ReasonPhrase, - httpResponse.RequestMessage.RequestUri, + httpResponse, cookies, - contentType, - httpResponse.Headers.Server.ToString(), - httpResponse.Content.Headers); + contentType); return new HttpResponse( deserializer, diff --git a/src/SpeakEasy/SingleUseStream.cs b/src/SpeakEasy/SingleUseStream.cs new file mode 100644 index 0000000..8daee80 --- /dev/null +++ b/src/SpeakEasy/SingleUseStream.cs @@ -0,0 +1,37 @@ +using System; +using System.IO; + +namespace SpeakEasy +{ + internal class SingleUseStream : IDisposable + { + private readonly Stream stream; + + private bool isConsumed; + + public SingleUseStream(Stream stream) + { + this.stream = stream; + } + + public Stream GetAndConsumeStream() + { + if (isConsumed) + { + throw new InvalidOperationException( + "An attempt was made to consume the same stream twice. This can happen if you " + + "try to do two things with an http response."); + } + + isConsumed = true; + + return stream; + } + + public void Dispose() + { + isConsumed = true; + stream.Dispose(); + } + } +}