Skip to content

Commit d3d2f00

Browse files
author
Bart Koelman
committed
Adds support for HEAD requests
1 parent d47661f commit d3d2f00

File tree

7 files changed

+71
-11
lines changed

7 files changed

+71
-11
lines changed

src/JsonApiDotNetCore/Controllers/JsonApiController.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,27 +42,31 @@ protected JsonApiController(IJsonApiOptions options, ILoggerFactory loggerFactor
4242

4343
/// <inheritdoc />
4444
[HttpGet]
45+
[HttpHead]
4546
public override async Task<IActionResult> GetAsync(CancellationToken cancellationToken)
4647
{
4748
return await base.GetAsync(cancellationToken);
4849
}
4950

5051
/// <inheritdoc />
5152
[HttpGet("{id}")]
53+
[HttpHead("{id}")]
5254
public override async Task<IActionResult> GetAsync(TId id, CancellationToken cancellationToken)
5355
{
5456
return await base.GetAsync(id, cancellationToken);
5557
}
5658

5759
/// <inheritdoc />
5860
[HttpGet("{id}/{relationshipName}")]
61+
[HttpHead("{id}/{relationshipName}")]
5962
public override async Task<IActionResult> GetSecondaryAsync(TId id, string relationshipName, CancellationToken cancellationToken)
6063
{
6164
return await base.GetSecondaryAsync(id, relationshipName, cancellationToken);
6265
}
6366

6467
/// <inheritdoc />
6568
[HttpGet("{id}/relationships/{relationshipName}")]
69+
[HttpHead("{id}/relationships/{relationshipName}")]
6670
public override async Task<IActionResult> GetRelationshipAsync(TId id, string relationshipName, CancellationToken cancellationToken)
6771
{
6872
return await base.GetRelationshipAsync(id, relationshipName, cancellationToken);

src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using System.Linq;
55
using System.Net;
66
using System.Net.Http;
7-
using System.Net.Http.Headers;
87
using System.Text;
98
using System.Threading.Tasks;
109
using JetBrains.Annotations;
@@ -16,6 +15,7 @@
1615
using Microsoft.AspNetCore.Mvc;
1716
using Microsoft.AspNetCore.Mvc.Controllers;
1817
using Microsoft.AspNetCore.Routing;
18+
using Microsoft.Net.Http.Headers;
1919
using Newtonsoft.Json;
2020

2121
namespace JsonApiDotNetCore.Middleware
@@ -46,7 +46,6 @@ public async Task InvokeAsync(HttpContext httpContext, IControllerResourceMappin
4646
ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider));
4747

4848
RouteValueDictionary routeValues = httpContext.GetRouteData().Values;
49-
5049
ResourceContext primaryResourceContext = CreatePrimaryResourceContext(httpContext, controllerResourceMapping, resourceContextProvider);
5150

5251
if (primaryResourceContext != null)
@@ -130,7 +129,7 @@ private static async Task<bool> ValidateAcceptHeaderAsync(MediaTypeHeaderValue a
130129

131130
foreach (string acceptHeader in acceptHeaders)
132131
{
133-
if (MediaTypeWithQualityHeaderValue.TryParse(acceptHeader, out MediaTypeWithQualityHeaderValue headerValue))
132+
if (MediaTypeHeaderValue.TryParse(acceptHeader, out MediaTypeHeaderValue headerValue))
134133
{
135134
headerValue.Quality = null;
136135

@@ -189,7 +188,7 @@ private static async Task FlushResponseAsync(HttpResponse httpResponse, JsonSeri
189188
private static void SetupResourceRequest(JsonApiRequest request, ResourceContext primaryResourceContext, RouteValueDictionary routeValues,
190189
IJsonApiOptions options, IResourceContextProvider resourceContextProvider, HttpRequest httpRequest)
191190
{
192-
request.IsReadOnly = httpRequest.Method == HttpMethod.Get.Method;
191+
request.IsReadOnly = httpRequest.Method == HttpMethod.Get.Method || httpRequest.Method == HttpMethod.Head.Method;
193192
request.Kind = EndpointKind.Primary;
194193
request.PrimaryResource = primaryResourceContext;
195194
request.PrimaryId = GetPrimaryRequestId(routeValues);

src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,15 @@ public async Task WriteAsync(OutputFormatterWriteContext context)
6363
response.StatusCode = (int)errorDocument.GetErrorStatusCode();
6464
}
6565

66+
if (context.HttpContext.Request.Method == HttpMethod.Head.Method)
67+
{
68+
responseContent = string.Empty;
69+
}
70+
6671
string url = context.HttpContext.Request.GetEncodedUrl();
67-
_traceWriter.LogMessage(() => $"Sending {response.StatusCode} response for request at '{url}' with body: <<{responseContent}>>");
72+
73+
_traceWriter.LogMessage(() =>
74+
$"Sending {response.StatusCode} response for {context.HttpContext.Request.Method} request at '{url}' with body: <<{responseContent}>>");
6875

6976
await writer.WriteAsync(responseContent);
7077
await writer.FlushAsync();
@@ -96,6 +103,11 @@ private string SerializeResponse(object contextObject, HttpStatusCode statusCode
96103
return _serializer.Serialize(contextObjectWrapped);
97104
}
98105

106+
private bool IsSuccessStatusCode(HttpStatusCode statusCode)
107+
{
108+
return new HttpResponseMessage(statusCode).IsSuccessStatusCode;
109+
}
110+
99111
private static object WrapErrors(object contextObject)
100112
{
101113
if (contextObject is IEnumerable<Error> errors)
@@ -110,10 +122,5 @@ private static object WrapErrors(object contextObject)
110122

111123
return contextObject;
112124
}
113-
114-
private bool IsSuccessStatusCode(HttpStatusCode statusCode)
115-
{
116-
return new HttpResponseMessage(statusCode).IsSuccessStatusCode;
117-
}
118125
}
119126
}

test/JsonApiDotNetCoreExampleTests/IntegrationTests/Logging/LoggingTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ public async Task Logs_response_body_at_Trace_level()
9999
loggerFactory.Logger.Messages.Should().NotBeEmpty();
100100

101101
loggerFactory.Logger.Messages.Should().ContainSingle(message => message.LogLevel == LogLevel.Trace &&
102-
message.Text.StartsWith("Sending 200 response for request at 'http://localhost/auditEntries' with body: <<", StringComparison.Ordinal));
102+
message.Text.StartsWith("Sending 200 response for GET request at 'http://localhost/auditEntries' with body: <<", StringComparison.Ordinal));
103103
}
104104

105105
[Fact]

test/JsonApiDotNetCoreExampleTests/IntegrationTests/Serialization/SerializationTests.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,44 @@ public SerializationTests(ExampleIntegrationTestContext<TestableStartup<Serializ
3939
options.AllowClientGeneratedIds = true;
4040
}
4141

42+
[Fact]
43+
public async Task Returns_no_body_for_successful_HEAD_request()
44+
{
45+
// Arrange
46+
Meeting meeting = _fakers.Meeting.Generate();
47+
48+
await _testContext.RunOnDatabaseAsync(async dbContext =>
49+
{
50+
dbContext.Meetings.Add(meeting);
51+
await dbContext.SaveChangesAsync();
52+
});
53+
54+
string route = "/meetings/" + meeting.StringId;
55+
56+
// Act
57+
(HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteHeadAsync<string>(route);
58+
59+
// Assert
60+
httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
61+
62+
responseDocument.Should().BeEmpty();
63+
}
64+
65+
[Fact]
66+
public async Task Returns_no_body_for_failed_HEAD_request()
67+
{
68+
// Arrange
69+
const string route = "/meetings/99999999";
70+
71+
// Act
72+
(HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteHeadAsync<string>(route);
73+
74+
// Assert
75+
httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound);
76+
77+
responseDocument.Should().BeEmpty();
78+
}
79+
4280
[Fact]
4381
public async Task Can_get_primary_resources_with_include()
4482
{

test/TestBuildingBlocks/IntegrationTest.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ public abstract class IntegrationTest
1414
{
1515
private static readonly IntegrationTestConfiguration IntegrationTestConfiguration = new IntegrationTestConfiguration();
1616

17+
public async Task<(HttpResponseMessage httpResponse, TResponseDocument responseDocument)> ExecuteHeadAsync<TResponseDocument>(string requestUrl,
18+
Action<HttpRequestHeaders> setRequestHeaders = null)
19+
{
20+
return await ExecuteRequestAsync<TResponseDocument>(HttpMethod.Head, requestUrl, null, null, setRequestHeaders);
21+
}
22+
1723
public async Task<(HttpResponseMessage httpResponse, TResponseDocument responseDocument)> ExecuteGetAsync<TResponseDocument>(string requestUrl,
1824
Action<HttpRequestHeaders> setRequestHeaders = null)
1925
{

test/UnitTests/Middleware/JsonApiRequestTests.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ namespace UnitTests.Middleware
1818
public sealed class JsonApiRequestTests
1919
{
2020
[Theory]
21+
[InlineData("HEAD", "/articles", true, EndpointKind.Primary, true)]
22+
[InlineData("HEAD", "/articles/1", false, EndpointKind.Primary, true)]
23+
[InlineData("HEAD", "/articles/1/author", false, EndpointKind.Secondary, true)]
24+
[InlineData("HEAD", "/articles/1/tags", true, EndpointKind.Secondary, true)]
25+
[InlineData("HEAD", "/articles/1/relationships/author", false, EndpointKind.Relationship, true)]
26+
[InlineData("HEAD", "/articles/1/relationships/tags", true, EndpointKind.Relationship, true)]
2127
[InlineData("GET", "/articles", true, EndpointKind.Primary, true)]
2228
[InlineData("GET", "/articles/1", false, EndpointKind.Primary, true)]
2329
[InlineData("GET", "/articles/1/author", false, EndpointKind.Secondary, true)]

0 commit comments

Comments
 (0)