Skip to content

Commit

Permalink
Refactored HttpClient usage into utility class
Browse files Browse the repository at this point in the history
  • Loading branch information
sblom committed Oct 2, 2020
1 parent 4aa1fe7 commit 5b37421
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 79 deletions.
110 changes: 57 additions & 53 deletions src/json-ld.net/Core/DocumentLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,34 @@ namespace JsonLD.Core
{
public class DocumentLoader
{
const int MAX_REDIRECTS = 20;
enum JsonLDContentType
{
JsonLD,
PlainJson,
Other
}

/// <summary>An HTTP Accept header that prefers JSONLD.</summary>
/// <remarks>An HTTP Accept header that prefers JSONLD.</remarks>
public const string AcceptHeader = "application/ld+json, application/json;q=0.9, application/javascript;q=0.5, text/javascript;q=0.5, text/plain;q=0.2, */*;q=0.1";
JsonLDContentType GetJsonLDContentType(string contentTypeStr)
{
JsonLDContentType contentType;

switch (contentTypeStr)
{
case "application/ld+json":
contentType = JsonLDContentType.JsonLD;
break;
// From RFC 6839, it looks like plain JSON is content tyep application/json and any MediaType ending in "+json".
case "application/json":
case string type when type.EndsWith("+json"):
contentType = JsonLDContentType.PlainJson;
break;
default:
contentType = JsonLDContentType.Other;
break;
}

return contentType;
}

/// <exception cref="JsonLDNet.Core.JsonLdError"></exception>
public RemoteDocument LoadDocument(string url)
Expand All @@ -31,68 +54,50 @@ public RemoteDocument LoadDocument(string url)
public async Task<RemoteDocument> LoadDocumentAsync(string url)
{
RemoteDocument doc = new RemoteDocument(url, null);

try
{
HttpResponseMessage httpResponseMessage;
using (HttpResponseMessage response = await JsonLD.Util.LDHttpClient.FetchAsync(url))
{

int redirects = 0;
int code;
string redirectedUrl = url;
var code = (int)response.StatusCode;

// Manually follow redirects because .NET Core refuses to auto-follow HTTPS->HTTP redirects.
do
{
HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, redirectedUrl);
httpRequestMessage.Headers.Add("Accept", AcceptHeader);
httpResponseMessage = await JSONUtils._HttpClient.SendAsync(httpRequestMessage);
if (httpResponseMessage.Headers.TryGetValues("Location", out var location))
if (code >= 400)
{
redirectedUrl = location.First();
throw new JsonLdError(JsonLdError.Error.LoadingDocumentFailed, $"HTTP {code} {url}");
}

code = (int)httpResponseMessage.StatusCode;
} while (redirects++ < MAX_REDIRECTS && code >= 300 && code < 400);
var finalUrl = response.RequestMessage.RequestUri.ToString();

if (redirects >= MAX_REDIRECTS)
{
throw new JsonLdError(JsonLdError.Error.LoadingDocumentFailed, $"Too many redirects - {url}");
}
var contentType = GetJsonLDContentType(response.Content.Headers.ContentType.MediaType);

if (code >= 400)
{
throw new JsonLdError(JsonLdError.Error.LoadingDocumentFailed, $"HTTP {code} {url}");
}

bool isJsonld = httpResponseMessage.Content.Headers.ContentType.MediaType == "application/ld+json";

// From RFC 6839, it looks like we should accept application/json and any MediaType ending in "+json".
if (httpResponseMessage.Content.Headers.ContentType.MediaType != "application/json" && !httpResponseMessage.Content.Headers.ContentType.MediaType.EndsWith("+json"))
{
throw new JsonLdError(JsonLdError.Error.LoadingDocumentFailed, url);
}
if (contentType == JsonLDContentType.Other)
{
throw new JsonLdError(JsonLdError.Error.LoadingDocumentFailed, url);
}

if (!isJsonld && httpResponseMessage.Headers.TryGetValues("Link", out var linkHeaders))
{
linkHeaders = linkHeaders.SelectMany((h) => h.Split(",".ToCharArray()))
.Select(h => h.Trim()).ToArray();
IEnumerable<string> linkedContexts = linkHeaders.Where(v => v.EndsWith("rel=\"http://www.w3.org/ns/json-ld#context\""));
if (linkedContexts.Count() > 1)
// For plain JSON, see if there's a context document linked in the HTTP response headers.
if (contentType == JsonLDContentType.PlainJson && response.Headers.TryGetValues("Link", out var linkHeaders))
{
throw new JsonLdError(JsonLdError.Error.MultipleContextLinkHeaders);
linkHeaders = linkHeaders.SelectMany((h) => h.Split(",".ToCharArray()))
.Select(h => h.Trim()).ToArray();
IEnumerable<string> linkedContexts = linkHeaders.Where(v => v.EndsWith("rel=\"http://www.w3.org/ns/json-ld#context\""));
if (linkedContexts.Count() > 1)
{
throw new JsonLdError(JsonLdError.Error.MultipleContextLinkHeaders);
}
string header = linkedContexts.First();
string linkedUrl = header.Substring(1, header.IndexOf(">") - 1);
string resolvedUrl = URL.Resolve(finalUrl, linkedUrl);
var remoteContext = this.LoadDocument(resolvedUrl);
doc.contextUrl = remoteContext.documentUrl;
doc.context = remoteContext.document;
}
string header = linkedContexts.First();
string linkedUrl = header.Substring(1, header.IndexOf(">") - 1);
string resolvedUrl = URL.Resolve(redirectedUrl, linkedUrl);
var remoteContext = this.LoadDocument(resolvedUrl);
doc.contextUrl = remoteContext.documentUrl;
doc.context = remoteContext.document;
}

Stream stream = await httpResponseMessage.Content.ReadAsStreamAsync();
Stream stream = await response.Content.ReadAsStreamAsync();

doc.DocumentUrl = redirectedUrl;
doc.Document = JSONUtils.FromInputStream(stream);
doc.DocumentUrl = finalUrl;
doc.Document = JSONUtils.FromInputStream(stream);
}
}
catch (JsonLdError)
{
Expand All @@ -104,6 +109,5 @@ public async Task<RemoteDocument> LoadDocumentAsync(string url)
}
return doc;
}

}
}
28 changes: 2 additions & 26 deletions src/json-ld.net/Util/JSONUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,6 @@ namespace JsonLD.Util
/// <author>tristan</author>
public class JSONUtils
{
const int MAX_REDIRECTS = 20;
static internal HttpClient _HttpClient = new HttpClient();

/// <summary>An HTTP Accept header that prefers JSONLD.</summary>
protected internal const string AcceptHeader = "application/ld+json, application/json;q=0.9, application/javascript;q=0.5, text/javascript;q=0.5, text/plain;q=0.2, */*;q=0.1";

static JSONUtils()
{
}
Expand Down Expand Up @@ -145,27 +139,9 @@ public static JToken FromURL(Uri url)
/// </exception>
public static async Task<JToken> FromURLAsync(Uri url)
{
HttpResponseMessage httpResponseMessage = null;
int redirects = 0;

// Manually follow redirects because .NET Core refuses to auto-follow HTTPS->HTTP redirects.
do
{
HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, url);
httpRequestMessage.Headers.Add("Accept", AcceptHeader);
httpResponseMessage = await _HttpClient.SendAsync(httpRequestMessage);
if (httpResponseMessage.Headers.TryGetValues("Location", out var location))
{
url = new Uri(location.First());
}
} while (redirects++ < MAX_REDIRECTS && (int)httpResponseMessage.StatusCode >= 300 && (int)httpResponseMessage.StatusCode < 400);

if (redirects >= MAX_REDIRECTS || (int)httpResponseMessage.StatusCode >= 400)
{
throw new InvalidOperationException("Couldn't load JSON from URL");
using (var response = await LDHttpClient.FetchAsync(url.ToString())) {
return FromInputStream(await response.Content.ReadAsStreamAsync());
}

return FromInputStream(await httpResponseMessage.Content.ReadAsStreamAsync());
}
}
}
52 changes: 52 additions & 0 deletions src/json-ld.net/Util/LDHttpClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;

namespace JsonLD.Util
{
internal static class LDHttpClient
{
const string ACCEPT_HEADER = "application/ld+json, application/json;q=0.9, application/javascript;q=0.5, text/javascript;q=0.5, text/plain;q=0.2, */*;q=0.1";
const int MAX_REDIRECTS = 20;

static HttpClient _hc;

static LDHttpClient()
{
_hc = new HttpClient();
_hc.DefaultRequestHeaders.Add("Accept", ACCEPT_HEADER);
}

static public async Task<HttpResponseMessage> FetchAsync(string url)
{
int redirects = 0;
int code;
string redirectedUrl = url;

HttpResponseMessage response;

// Manually follow redirects because .NET Core refuses to auto-follow HTTPS->HTTP redirects.
do
{
HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, redirectedUrl);
response = await _hc.SendAsync(httpRequestMessage);
if (response.Headers.TryGetValues("Location", out var location))
{
redirectedUrl = location.First();
}

code = (int)response.StatusCode;
} while (redirects++ < MAX_REDIRECTS && code >= 300 && code < 400);

if (redirects >= MAX_REDIRECTS)
{
throw new HttpRequestException("Too many redirects");
}

return response;
}
}
}

0 comments on commit 5b37421

Please sign in to comment.