Skip to content

Commit 8165af7

Browse files
committed
Check status code of HTTP request and throw corresponding exceptions
1 parent d3e35e5 commit 8165af7

3 files changed

Lines changed: 113 additions & 54 deletions

File tree

‎MoodleLti.DependencyInjection/ServiceCollectionExtensions.cs‎

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace Microsoft.Extensions.DependencyInjection
99
public static class ServiceCollectionExtensions
1010
{
1111
/// <summary>
12-
/// Adds a Moodle LTI API service. This services requires presence of <see cref="System.Net.Http.IHttpClientFactory"/> and <see cref="Microsoft.Extensions.Options.IOptions{MoodleLti.Options.MoodleLtiOptions}"/>.
12+
/// Adds a Moodle LTI API service. This services requires presence of <see cref="System.Net.Http.IHttpClientFactory"/> and <see cref="Options.IOptions{MoodleLtiOptions}"/>.
1313
/// </summary>
1414
/// <param name="services">Service collection.</param>
1515
/// <returns></returns>
@@ -20,29 +20,23 @@ public static IServiceCollection AddMoodleLtiApi(this IServiceCollection service
2020
}
2121

2222
/// <summary>
23-
/// Adds a Moodle gradebook service. This services requires presence of <see cref="System.Net.Http.IHttpClientFactory"/> and <see cref="Microsoft.Extensions.Options.IOptions{MoodleLti.Options.MoodleLtiOptions}"/>.
23+
/// Adds a Moodle gradebook service. This services requires presence of <see cref="IMoodleLtiApi"/>, <see cref="System.Net.Http.IHttpClientFactory"/> and <see cref="Options.IOptions{MoodleLtiOptions}"/>.
2424
/// </summary>
2525
/// <param name="services">Service collection.</param>
2626
/// <returns></returns>
2727
public static IServiceCollection AddMoodleGradebook(this IServiceCollection services)
2828
{
29-
// Make sure LTI API is present
30-
services.TryAddTransient<IMoodleLtiApi, MoodleLtiApi>();
31-
3229
// Add service
3330
return services.AddTransient<IMoodleGradebook, MoodleGradebook>();
3431
}
3532

3633
/// <summary>
37-
/// Adds a cached Moodle gradebook service. This services requires presence of <see cref="System.Net.Http.IHttpClientFactory"/> and <see cref="Microsoft.Extensions.Options.IOptions{MoodleLti.Options.MoodleLtiOptions}"/>.
34+
/// Adds a cached Moodle gradebook service. This services requires presence of <see cref="IMoodleLtiApi"/>, <see cref="System.Net.Http.IHttpClientFactory"/> and <see cref="Options.IOptions{MoodleLtiOptions}"/>.
3835
/// </summary>
3936
/// <param name="services">Service collection.</param>
4037
/// <returns></returns>
4138
public static IServiceCollection AddCachedMoodleGradebook(this IServiceCollection services)
4239
{
43-
// Make sure LTI API is present
44-
services.TryAddTransient<IMoodleLtiApi, MoodleLtiApi>();
45-
4640
// Add service
4741
return services.AddSingleton<IMoodleGradebook, CachedMoodleGradebook>();
4842
}

‎MoodleLti/Extensions/HttpResponseMessageExtensions.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ internal static class HttpResponseMessageExtensions
1717
public static async Task<string> ReadBody(this HttpResponseMessage response)
1818
{
1919
// Read response body
20-
using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
20+
await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
2121
using var streamReader = new StreamReader(stream);
2222
return await streamReader.ReadToEndAsync().ConfigureAwait(false);
2323
}

‎MoodleLti/MoodleLtiApi.cs‎

Lines changed: 109 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System;
1010
using System.Collections.Generic;
1111
using System.Globalization;
12+
using System.Linq;
1213
using System.Net;
1314
using System.Net.Http;
1415
using System.Net.Http.Headers;
@@ -71,8 +72,8 @@ public async Task<List<MoodleLtiLineItem>> GetLineItemsAsync()
7172
string url = $"{_serviceUrl}/lineitems?{_queryStringPrefix}";
7273

7374
// Perform request and parse response body
74-
string response = await DoGetRequestAsync(url, "application/vnd.ims.lis.v2.lineitemcontainer+json");
75-
var lineItems = JsonConvert.DeserializeObject<List<MoodleLtiLineItem>>(response);
75+
(string responseBody, HttpStatusCode _) = await DoGetRequestAsync(url, "application/vnd.ims.lis.v2.lineitemcontainer+json", new[] { HttpStatusCode.OK });
76+
var lineItems = JsonConvert.DeserializeObject<List<MoodleLtiLineItem>>(responseBody);
7677

7778
// Generate numeric IDs
7879
foreach(var lineItem in lineItems)
@@ -88,10 +89,10 @@ public async Task<int> CreateLineItemAsync(MoodleLtiLineItem lineItem)
8889

8990
// Serialize line item
9091
string serializedLineItem = JsonConvert.SerializeObject(lineItem, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
91-
string result = await DoPostRequestAsync(url, serializedLineItem, "application/vnd.ims.lis.v2.lineitem+json");
92+
(string responseBody, HttpStatusCode _) = await DoPostRequestAsync(url, serializedLineItem, "application/vnd.ims.lis.v2.lineitem+json", new[] { HttpStatusCode.Created });
9293

9394
// Try to deserialize result
94-
MoodleLtiLineItem resultLineItem = JsonConvert.DeserializeObject<MoodleLtiLineItem>(result);
95+
MoodleLtiLineItem resultLineItem = JsonConvert.DeserializeObject<MoodleLtiLineItem>(responseBody);
9596

9697
// Extract ID
9798
return GetNumericLineItemId(resultLineItem.StringId);
@@ -103,8 +104,8 @@ public async Task<MoodleLtiLineItem> GetLineItemAsync(int id)
103104
string url = $"{_serviceUrl}/lineitems/{id}/lineitem?{_queryStringPrefix}";
104105

105106
// Retrieve line item
106-
string response = await DoGetRequestAsync(url, "application/vnd.ims.lis.v2.lineitem+json");
107-
var lineItem = JsonConvert.DeserializeObject<MoodleLtiLineItem>(response);
107+
(string responseBody, HttpStatusCode _) = await DoGetRequestAsync(url, "application/vnd.ims.lis.v2.lineitem+json", new[] { HttpStatusCode.OK });
108+
var lineItem = JsonConvert.DeserializeObject<MoodleLtiLineItem>(responseBody);
108109
lineItem.Id = GetNumericLineItemId(lineItem.StringId);
109110

110111
return lineItem;
@@ -118,9 +119,7 @@ public async Task UpdateLineItemAsync(MoodleLtiLineItem lineItem)
118119
// Serialize line item
119120
lineItem.StringId = url;
120121
string serializedLineItem = JsonConvert.SerializeObject(lineItem, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
121-
string result = await DoPutRequestAsync(url, serializedLineItem, "application/vnd.ims.lis.v2.lineitem+json");
122-
123-
// TODO check result
122+
await DoPutRequestAsync(url, serializedLineItem, "application/vnd.ims.lis.v2.lineitem+json", new[] { HttpStatusCode.OK });
124123
}
125124

126125
public async Task DeleteLineItemAsync(int id)
@@ -129,9 +128,7 @@ public async Task DeleteLineItemAsync(int id)
129128
string url = $"{_serviceUrl}/lineitems/{id}/lineitem?{_queryStringPrefix}";
130129

131130
// Delete line item
132-
string result = await DoDeleteRequestAsync(url);
133-
134-
// TODO check result
131+
await DoDeleteRequestAsync(url, new[] { HttpStatusCode.NoContent });
135132
}
136133

137134
public async Task UpdateScoreAsync(int lineItemId, MoodleLtiScore score)
@@ -141,18 +138,17 @@ public async Task UpdateScoreAsync(int lineItemId, MoodleLtiScore score)
141138

142139
// Serialize and send score
143140
string serializedScore = JsonConvert.SerializeObject(score, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
144-
string result = await DoPostRequestAsync(url, serializedScore, "application/vnd.ims.lis.v1.score+json");
145-
146-
// TODO check result
141+
await DoPostRequestAsync(url, serializedScore, "application/vnd.ims.lis.v1.score+json", new[] { HttpStatusCode.OK });
147142
}
148143

149144
/// <summary>
150-
/// Performs a GET request and returns the response body, if it was successful.
145+
/// Performs a GET request and returns the response body and its status code.
151146
/// </summary>
152147
/// <param name="url">The target URL.</param>
153148
/// <param name="expectedContentType">The expected response content type.</param>
154-
/// <returns></returns>
155-
private async Task<string> DoGetRequestAsync(string url, string expectedContentType)
149+
/// <param name="expectedStatusCodes">The expected status codes. All other status codes trigger an exception.</param>
150+
/// <exception cref="MoodleLtiException">Thrown when an unexpected HTTP status code is returned.</exception>
151+
private async Task<(string responseBody, HttpStatusCode statusCode)> DoGetRequestAsync(string url, string expectedContentType, HttpStatusCode[] expectedStatusCodes)
156152
{
157153
// Assign content type
158154
_httpClient.DefaultRequestHeaders.Accept.Clear();
@@ -166,23 +162,31 @@ private async Task<string> DoGetRequestAsync(string url, string expectedContentT
166162
await SecuredClient.SignRequest(_httpClient, reqMessage, _consumerKey, _sharedSecret, SignatureMethod.HmacSha1);
167163

168164
// Send HTTP request and retrieve response
169-
// TODO exception handling
170-
using(var response = await _httpClient.SendAsync(reqMessage))
171-
{
172-
if(response.StatusCode == HttpStatusCode.OK)
173-
return await response.ReadBody();
174-
}
175-
return default;
165+
using var response = await _httpClient.SendAsync(reqMessage);
166+
string responseBody = await response.ReadBody();
167+
if(expectedStatusCodes.Contains(response.StatusCode))
168+
return (responseBody, response.StatusCode);
169+
170+
// An error occured
171+
throw CreateExceptionFromFailedRequest(
172+
response.StatusCode,
173+
expectedStatusCodes,
174+
response.RequestMessage.Headers.ToString(),
175+
string.Empty,
176+
response.Headers.ToString(),
177+
responseBody
178+
);
176179
}
177180

178181
/// <summary>
179-
/// Performs a POST request and returns the response body, if it was successful.
182+
/// Performs a POST request and returns the response body and its status code.
180183
/// </summary>
181184
/// <param name="url">The target URL.</param>
182185
/// <param name="body">The POST body.</param>
183186
/// <param name="bodyContentType">The content type of the body.</param>
184-
/// <returns></returns>
185-
private async Task<string> DoPostRequestAsync(string url, string body, string bodyContentType)
187+
/// <param name="expectedStatusCodes">The expected status codes. All other status codes trigger an exception.</param>
188+
/// <exception cref="MoodleLtiException">Thrown when an unexpected HTTP status code is returned.</exception>
189+
private async Task<(string responseBody, HttpStatusCode statusCode)> DoPostRequestAsync(string url, string body, string bodyContentType, HttpStatusCode[] expectedStatusCodes)
186190
{
187191
// Sign request object
188192
var encodedBody = new StringContent(body, Encoding.UTF8, bodyContentType);
@@ -193,19 +197,31 @@ private async Task<string> DoPostRequestAsync(string url, string body, string bo
193197
await SecuredClient.SignRequest(_httpClient, reqMessage, _consumerKey, _sharedSecret, SignatureMethod.HmacSha1);
194198

195199
// Send HTTP request and retrieve response
196-
// TODO exception handling
197200
using var response = await _httpClient.SendAsync(reqMessage);
198-
return await response.ReadBody();
201+
string responseBody = await response.ReadBody();
202+
if(expectedStatusCodes.Contains(response.StatusCode))
203+
return (responseBody, response.StatusCode);
204+
205+
// An error occured
206+
throw CreateExceptionFromFailedRequest(
207+
response.StatusCode,
208+
expectedStatusCodes,
209+
response.RequestMessage.Headers.ToString(),
210+
body,
211+
response.Headers.ToString(),
212+
responseBody
213+
);
199214
}
200215

201216
/// <summary>
202-
/// Performs a PUT request and returns the response body, if it was successful.
217+
/// Performs a PUT request and returns the response body and its status code.
203218
/// </summary>
204219
/// <param name="url">The target URL.</param>
205220
/// <param name="body">The PUT body.</param>
206221
/// <param name="bodyContentType">The content type of the body.</param>
207-
/// <returns></returns>
208-
private async Task<string> DoPutRequestAsync(string url, string body, string bodyContentType)
222+
/// <param name="expectedStatusCodes">The expected status codes. All other status codes trigger an exception.</param>
223+
/// <exception cref="MoodleLtiException">Thrown when an unexpected HTTP status code is returned.</exception>
224+
private async Task<(string responseBody, HttpStatusCode statusCode)> DoPutRequestAsync(string url, string body, string bodyContentType, HttpStatusCode[] expectedStatusCodes)
209225
{
210226
// Sign request object
211227
var encodedBody = new StringContent(body, Encoding.UTF8, bodyContentType);
@@ -216,17 +232,29 @@ private async Task<string> DoPutRequestAsync(string url, string body, string bod
216232
await SecuredClient.SignRequest(_httpClient, reqMessage, _consumerKey, _sharedSecret, SignatureMethod.HmacSha1);
217233

218234
// Send HTTP request and retrieve response
219-
// TODO exception handling
220235
using var response = await _httpClient.SendAsync(reqMessage);
221-
return await response.ReadBody();
236+
string responseBody = await response.ReadBody();
237+
if(expectedStatusCodes.Contains(response.StatusCode))
238+
return (responseBody, response.StatusCode);
239+
240+
// An error occured
241+
throw CreateExceptionFromFailedRequest(
242+
response.StatusCode,
243+
expectedStatusCodes,
244+
response.RequestMessage.Headers.ToString(),
245+
body,
246+
response.Headers.ToString(),
247+
responseBody
248+
);
222249
}
223250

224251
/// <summary>
225-
/// Performs a DELETE request and returns the response body, if it was successful.
252+
/// Performs a DELETE request and returns the response body and its status code.
226253
/// </summary>
227254
/// <param name="url">The target URL.</param>
228-
/// <returns></returns>
229-
private async Task<string> DoDeleteRequestAsync(string url)
255+
/// <param name="expectedStatusCodes">The expected status codes. All other status codes trigger an exception.</param>
256+
/// <exception cref="MoodleLtiException">Thrown when an unexpected HTTP status code is returned.</exception>
257+
private async Task<(string responseBody, HttpStatusCode statusCode)> DoDeleteRequestAsync(string url, HttpStatusCode[] expectedStatusCodes)
230258
{
231259
// Sign request object
232260
var reqMessage = new HttpRequestMessage(HttpMethod.Delete, url)
@@ -236,12 +264,49 @@ private async Task<string> DoDeleteRequestAsync(string url)
236264
await SecuredClient.SignRequest(_httpClient, reqMessage, _consumerKey, _sharedSecret, SignatureMethod.HmacSha1);
237265

238266
// Send HTTP request and retrieve response
239-
// TODO exception handling
240-
using(var response = await _httpClient.SendAsync(reqMessage))
241-
{
242-
// Should return 204 NoContent
243-
}
244-
return default;
267+
using var response = await _httpClient.SendAsync(reqMessage);
268+
string responseBody = await response.ReadBody();
269+
if(expectedStatusCodes.Contains(response.StatusCode))
270+
return (responseBody, response.StatusCode);
271+
272+
// An error occured
273+
throw CreateExceptionFromFailedRequest(
274+
response.StatusCode,
275+
expectedStatusCodes,
276+
response.RequestMessage.Headers.ToString(),
277+
string.Empty,
278+
response.Headers.ToString(),
279+
responseBody
280+
);
281+
}
282+
283+
/// <summary>
284+
/// Creates a new <see cref="MoodleLtiException"/> from the given HTTP request data.
285+
/// </summary>
286+
/// <param name="statusCode">Server status code.</param>
287+
/// <param name="expectedStatusCodes">Expected server status codes.</param>
288+
/// <param name="requestHeaders">Request headers.</param>
289+
/// <param name="requestBody">Request body.</param>
290+
/// <param name="responseHeaders">Response headers.</param>
291+
/// <param name="responseBody">Response body.</param>
292+
/// <returns></returns>
293+
private static MoodleLtiException CreateExceptionFromFailedRequest(
294+
HttpStatusCode statusCode,
295+
HttpStatusCode[] expectedStatusCodes,
296+
string requestHeaders,
297+
string requestBody,
298+
string responseHeaders,
299+
string responseBody)
300+
{
301+
string expectedStatusCodesString = string.Join(", ", expectedStatusCodes);
302+
var exception = new MoodleLtiException($"Unexpected HTTP status: {statusCode} (expected: {expectedStatusCodesString})");
303+
exception.Data.Add("StatusCode", statusCode);
304+
exception.Data.Add("StatusCodesExpected", expectedStatusCodesString);
305+
exception.Data.Add("RequestHeaders", requestHeaders);
306+
exception.Data.Add("RequestBody", requestBody);
307+
exception.Data.Add("ResponseHeaders", responseHeaders);
308+
exception.Data.Add("ResponseBody", responseBody);
309+
return exception;
245310
}
246311

247312
/// <summary>

0 commit comments

Comments
 (0)