• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

loresoft / ProblemJson / 29760706390

20 Jul 2026 04:41PM UTC coverage: 89.815%. First build
29760706390

push

github

pwelter34
first commit

34 of 40 branches covered (85.0%)

Branch coverage included in aggregate %.

63 of 68 new or added lines in 2 files covered. (92.65%)

63 of 68 relevant lines covered (92.65%)

3.35 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.29
/src/ProblemJson/ProblemJsonExtensions.cs
1
using System.Text.Json;
2

3
namespace System.Net.Http.Json;
4

5
/// <summary>
6
/// Extension methods for <see cref="HttpResponseMessage"/> to detect, read, and throw on
7
/// <c>application/problem+json</c> responses, as defined by
8
/// <see href="https://www.rfc-editor.org/rfc/rfc9457">RFC 9457</see>.
9
/// </summary>
10
/// <example>
11
/// The following example sends a request and converts a problem details response into an exception:
12
/// <code>
13
/// using var client = new HttpClient();
14
/// using var response = await client.GetAsync("https://api.example.com/orders/42");
15
///
16
/// // Throws a ProblemDetailsException if the body is application/problem+json.
17
/// await response.ThrowIfProblemJsonAsync();
18
/// response.EnsureSuccessStatusCode();
19
/// </code>
20
/// </example>
21
public static class ProblemJsonExtensions
22
{
23
#if NET8_0_OR_GREATER
24
    private const string ProblemJsonMediaType = System.Net.Mime.MediaTypeNames.Application.ProblemJson;
25
#else
26
    private const string ProblemJsonMediaType = "application/problem+json";
27
#endif
28

29
    /// <summary>
30
    /// Determines whether the response body is an <c>application/problem+json</c> payload
31
    /// by inspecting the <c>Content-Type</c> header.
32
    /// </summary>
33
    /// <param name="response">The HTTP response message to inspect.</param>
34
    /// <returns>
35
    /// <see langword="true"/> when the content type is <c>application/problem+json</c>; otherwise <see langword="false"/>.
36
    /// </returns>
37
    /// <exception cref="ArgumentNullException"><paramref name="response"/> is <see langword="null"/>.</exception>
38
    /// <example>
39
    /// <code>
40
    /// if (response.IsProblemJson())
41
    /// {
42
    ///     var problem = await response.ReadProblemJsonAsync();
43
    /// }
44
    /// </code>
45
    /// </example>
46
    public static bool IsProblemJson(this HttpResponseMessage response)
47
    {
48
        if (response is null)
13✔
49
            throw new ArgumentNullException(nameof(response));
1✔
50

51
        var mediaType = response.Content?.Headers.ContentType?.MediaType;
12!
52

53
        return string.Equals(mediaType, ProblemJsonMediaType, StringComparison.OrdinalIgnoreCase);
12✔
54
    }
55

56
    /// <summary>
57
    /// Reads and deserializes the response body into a <see cref="ProblemDetails"/> instance
58
    /// using the source-generated serializer context.
59
    /// </summary>
60
    /// <param name="response">The HTTP response message to read.</param>
61
    /// <param name="cancellationToken">A token to cancel the operation.</param>
62
    /// <returns>
63
    /// The deserialized <see cref="ProblemDetails"/>, or <see langword="null"/> when the response is not
64
    /// an <c>application/problem+json</c> payload.
65
    /// </returns>
66
    /// <exception cref="ArgumentNullException"><paramref name="response"/> is <see langword="null"/>.</exception>
67
    /// <example>
68
    /// <code>
69
    /// var problem = await response.ReadProblemJsonAsync();
70
    /// if (problem is not null)
71
    ///     Console.WriteLine($"{problem.Title}: {problem.Detail}");
72
    /// </code>
73
    /// </example>
74
    public static async Task<ProblemDetails?> ReadProblemJsonAsync(
75
        this HttpResponseMessage response,
76
        CancellationToken cancellationToken = default)
77
    {
78
        if (response is null)
4✔
79
            throw new ArgumentNullException(nameof(response));
1✔
80

81
        if (!response.IsProblemJson())
3✔
82
            return null;
1✔
83

84
        return await response.Content
2✔
85
            .ReadFromJsonAsync(ProblemDetailsSerializerContext.Default.ProblemDetails, cancellationToken)
2✔
86
            .ConfigureAwait(false);
2✔
87
    }
3✔
88

89
    /// <summary>
90
    /// Reads and deserializes the response body into a <see cref="ProblemDetails"/> instance
91
    /// using the specified serializer options.
92
    /// </summary>
93
    /// <param name="response">The HTTP response message to read.</param>
94
    /// <param name="options">The serializer options to use, or <see langword="null"/> to use the default options.</param>
95
    /// <param name="cancellationToken">A token to cancel the operation.</param>
96
    /// <returns>
97
    /// The deserialized <see cref="ProblemDetails"/>, or <see langword="null"/> when the response is not
98
    /// an <c>application/problem+json</c> payload.
99
    /// </returns>
100
    /// <exception cref="ArgumentNullException"><paramref name="response"/> is <see langword="null"/>.</exception>
101
    /// <example>
102
    /// <code>
103
    /// var options = new JsonSerializerOptions(JsonSerializerDefaults.Web);
104
    /// var problem = await response.ReadProblemJsonAsync(options);
105
    /// </code>
106
    /// </example>
107
    public static async Task<ProblemDetails?> ReadProblemJsonAsync(
108
        this HttpResponseMessage response,
109
        JsonSerializerOptions? options = null,
110
        CancellationToken cancellationToken = default)
111
    {
112
        if (response is null)
2!
NEW
113
            throw new ArgumentNullException(nameof(response));
×
114

115
        if (!response.IsProblemJson())
2!
NEW
116
            return null;
×
117

118
        return await response.Content
2✔
119
            .ReadFromJsonAsync<ProblemDetails>(options, cancellationToken)
2✔
120
            .ConfigureAwait(false);
2✔
121
    }
2✔
122

123
    /// <summary>
124
    /// Throws a <see cref="ProblemDetailsException"/> when the response body is an
125
    /// <c>application/problem+json</c> payload. Does not otherwise validate the status code;
126
    /// callers should still call <see cref="HttpResponseMessage.EnsureSuccessStatusCode"/> as needed.
127
    /// </summary>
128
    /// <param name="response">The HTTP response message to inspect.</param>
129
    /// <param name="cancellationToken">A token to cancel the operation.</param>
130
    /// <returns>The same <paramref name="response"/> instance for chaining.</returns>
131
    /// <exception cref="ArgumentNullException"><paramref name="response"/> is <see langword="null"/>.</exception>
132
    /// <exception cref="ProblemDetailsException">Thrown when the response is a problem details payload.</exception>
133
    /// <example>
134
    /// <code>
135
    /// using var response = await client.GetAsync("https://api.example.com/orders/42");
136
    /// await response.ThrowIfProblemJsonAsync();
137
    /// response.EnsureSuccessStatusCode();
138
    /// </code>
139
    /// </example>
140
    public static async Task<HttpResponseMessage> ThrowIfProblemJsonAsync(
141
        this HttpResponseMessage response,
142
        CancellationToken cancellationToken = default)
143
    {
144
        if (response is null)
3✔
145
            throw new ArgumentNullException(nameof(response));
1✔
146

147
        if (!response.IsProblemJson())
2✔
148
            return response;
1✔
149

150
        var problemDetails = await response
1✔
151
                .ReadProblemJsonAsync(cancellationToken)
1✔
152
                .ConfigureAwait(false);
1✔
153

154
#if NET5_0_OR_GREATER
155
        throw new ProblemDetailsException(
1✔
156
            message: null,
1✔
157
            inner: null,
1✔
158
            statusCode: response.StatusCode,
1✔
159
            problemDetails: problemDetails);
1✔
160
#else
161
        throw new ProblemDetailsException(
162
            message: null,
163
            inner: null,
164
            problemDetails: problemDetails);
165
#endif
166
    }
1✔
167

168
    /// <summary>
169
    /// Throws a <see cref="ProblemDetailsException"/> when the response body is an
170
    /// <c>application/problem+json</c> payload. Does not otherwise validate the status code;
171
    /// callers should still call <see cref="HttpResponseMessage.EnsureSuccessStatusCode"/> as needed.
172
    /// </summary>
173
    /// <param name="response">The HTTP response message to inspect.</param>
174
    /// <param name="options">The serializer options to use, or <see langword="null"/> to use the default options.</param>
175
    /// <param name="cancellationToken">A token to cancel the operation.</param>
176
    /// <returns>The same <paramref name="response"/> instance for chaining.</returns>
177
    /// <exception cref="ArgumentNullException"><paramref name="response"/> is <see langword="null"/>.</exception>
178
    /// <exception cref="ProblemDetailsException">Thrown when the response is a problem details payload.</exception>
179
    /// <example>
180
    /// <code>
181
    /// var options = new JsonSerializerOptions(JsonSerializerDefaults.Web);
182
    /// using var response = await client.GetAsync("https://api.example.com/orders/42");
183
    /// await response.ThrowIfProblemJsonAsync(options);
184
    /// </code>
185
    /// </example>
186
    public static async Task<HttpResponseMessage> ThrowIfProblemJsonAsync(
187
        this HttpResponseMessage response,
188
        JsonSerializerOptions? options = null,
189
        CancellationToken cancellationToken = default)
190
    {
191
        if (response is null)
1!
NEW
192
            throw new ArgumentNullException(nameof(response));
×
193

194
        if (!response.IsProblemJson())
1!
NEW
195
            return response;
×
196

197
        var problemDetails = await response
1✔
198
                .ReadProblemJsonAsync(options, cancellationToken)
1✔
199
                .ConfigureAwait(false);
1✔
200

201
#if NET5_0_OR_GREATER
202
        throw new ProblemDetailsException(
1✔
203
            message: null,
1✔
204
            inner: null,
1✔
205
            statusCode: response.StatusCode,
1✔
206
            problemDetails: problemDetails);
1✔
207
#else
208
        throw new ProblemDetailsException(
209
            message: null,
210
            inner: null,
211
            problemDetails: problemDetails);
212
#endif
NEW
213
    }
×
214
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc