Skip to content

Commit 5e82d77

Browse files
authored
Merge pull request #16 from JSkimming/json-read-error
Handle invalid JSON on error responses
2 parents 0abf386 + f5f9dfe commit 5e82d77

File tree

3 files changed

+143
-5
lines changed

3 files changed

+143
-5
lines changed

src/Tesla.NET/Requests/HttpClientExtensions.cs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ namespace Tesla.NET.Requests
77
using System.Collections.Generic;
88
using System.IO;
99
using System.Linq;
10+
using System.Net;
1011
using System.Net.Http;
1112
using System.Net.Http.Headers;
1213
using System.Threading;
@@ -412,10 +413,20 @@ private static async Task<IMessageResponse<T>> ReadFailureResponseAsync<T>(
412413
if (responseMessage == null)
413414
throw new ArgumentNullException(nameof(responseMessage));
414415

415-
JObject rawJson =
416-
IsContentJson(responseMessage)
417-
? await ReadJsonAsync(responseMessage, cancellationToken).ConfigureAwait(false)
418-
: null;
416+
JObject rawJson = null;
417+
try
418+
{
419+
// Check the content is JSON, and the response was not Unauthorized as the API returns a Content-Type
420+
// of application/json, but not JSON content.
421+
rawJson =
422+
IsContentJson(responseMessage) && responseMessage.StatusCode != HttpStatusCode.Unauthorized
423+
? await ReadJsonAsync(responseMessage, cancellationToken).ConfigureAwait(false)
424+
: null;
425+
}
426+
catch
427+
{
428+
// ignored to allow the error code to be returned.
429+
}
419430

420431
IMessageResponse<T> response = new MessageResponse<T>(responseMessage.StatusCode, rawJson);
421432
return response;

test/Tesla.NET.Tests/HttpHandlers/ForcedAsyncStream.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace Tesla.NET.HttpHandlers
1111
using System.Threading.Tasks;
1212

1313
/// <summary>
14-
/// Adds a yield to <see cref="ReadAsync"/> to ensure streams are read asynchronously.
14+
/// Adds a <see cref="Task.Yield"/> to <see cref="ReadAsync"/> to ensure streams are read asynchronously.
1515
/// </summary>
1616
internal class ForcedAsyncStream : Stream
1717
{
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// Copyright (c) 2018 James Skimming. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
3+
4+
namespace Tesla.NET
5+
{
6+
using System;
7+
using System.Collections.Generic;
8+
using System.IO;
9+
using System.Linq;
10+
using System.Net;
11+
using System.Net.Http;
12+
using System.Net.Http.Headers;
13+
using System.Text;
14+
using System.Threading.Tasks;
15+
using AutoFixture;
16+
using FluentAssertions;
17+
using Tesla.NET.HttpHandlers;
18+
using Tesla.NET.Models;
19+
using Xunit;
20+
using Xunit.Abstractions;
21+
22+
public abstract class InvalidJsonResponseTestsBase : ClientRequestContext
23+
{
24+
private readonly long _vehicleId;
25+
private readonly HttpStatusCode _statusCode;
26+
27+
protected InvalidJsonResponseTestsBase(ITestOutputHelper output, HttpStatusCode statusCode)
28+
: base(output, useCustomBaseUri : false)
29+
{
30+
// Arrange
31+
_statusCode = statusCode;
32+
_vehicleId = Fixture.Create<long>();
33+
Handler.Response = CreateResponse(statusCode);
34+
}
35+
36+
private static HttpResponseMessage CreateResponse(HttpStatusCode statusCode)
37+
{
38+
string stringContent = "0" + Environment.NewLine + Environment.NewLine;
39+
var stream = new MemoryStream(32);
40+
using (var streamWriter = new StreamWriter(stream, Encoding.UTF8, 10240, leaveOpen: true))
41+
{
42+
streamWriter.Write(stringContent);
43+
streamWriter.Close();
44+
}
45+
46+
stream.Seek(0, SeekOrigin.Begin);
47+
var httpContent = new ForcedAsyncStreamContent(stream)
48+
{
49+
Headers =
50+
{
51+
ContentType = new MediaTypeHeaderValue("application/json"),
52+
}
53+
};
54+
55+
HttpResponseMessage response = new HttpResponseMessage(statusCode)
56+
{
57+
Content = httpContent,
58+
};
59+
return response;
60+
}
61+
62+
[Fact]
63+
public async Task It_should_return_just_the_HTTP_status_Code()
64+
{
65+
// Act
66+
IMessageResponse actual = await Sut.GetVehicleStateAsync(_vehicleId, AccessToken).ConfigureAwait(false);
67+
68+
// Assert
69+
actual.HttpStatusCode.Should().Be(_statusCode);
70+
}
71+
72+
[Fact]
73+
public void It_should_not_throw_an_exception()
74+
{
75+
// Act
76+
Func<Task> action = () => Sut.GetVehicleStateAsync(_vehicleId, AccessToken);
77+
78+
// Assert
79+
action.ShouldNotThrow();
80+
}
81+
}
82+
83+
/// <summary>
84+
/// The Tesla Owners API returns a body of <c>0</c> in an unauthorized response, but still states the
85+
/// <c>Content-Type</c> is <c>application/json</c>. These test is defined to ensure the Tesla.NET client does not
86+
/// break in such circumstances.
87+
/// </summary>
88+
/// <example>
89+
/// The following is an example 401 Unauthorized response.
90+
/// <code>
91+
/// HTTP/1.1 401 Unauthorized
92+
/// Server: nginx
93+
/// Date: Thu, 01 Feb 2018 07:58:39 GMT
94+
/// Content-Type: application/json
95+
/// Transfer-Encoding: chunked
96+
/// Connection: keep-alive
97+
/// X-Frame-Options: SAMEORIGIN
98+
/// X-XSS-Protection: 1; mode=block
99+
/// X-Content-Type-Options: nosniff
100+
/// Cache-Control: no-store
101+
/// Pragma: no-cache
102+
/// WWW-Authenticate: Bearer realm="Doorkeeper", error="invalid_token", error_description="The access token is invalid"
103+
/// X-Request-Id: b05b4cc2-7684-49bb-b2cc-57c548d4a2cf
104+
/// X-Runtime: 0.024554
105+
///
106+
/// 0
107+
/// </code>
108+
/// </example>
109+
public class When_getting_invalid_JSON_in_an_unauthorised_response : InvalidJsonResponseTestsBase
110+
{
111+
public When_getting_invalid_JSON_in_an_unauthorised_response(ITestOutputHelper output)
112+
: base(output, HttpStatusCode.Unauthorized)
113+
{
114+
}
115+
}
116+
117+
/// <summary>
118+
/// This simply tests any other error response with invalid JKSON doesn't throw an exception.
119+
/// </summary>
120+
public class When_getting_invalid_JSON_in_an_error_response : InvalidJsonResponseTestsBase
121+
{
122+
public When_getting_invalid_JSON_in_an_error_response(ITestOutputHelper output)
123+
: base(output, HttpStatusCode.Conflict)
124+
{
125+
}
126+
}
127+
}

0 commit comments

Comments
 (0)