Skip to content

Commit 01e98c2

Browse files
RenderMichaelnvborisenko
authored andcommitted
[dotnet] Move Response constructors towards immutability (SeleniumHQ#14998)
Moves us towards immutability of the Response type. Eventually, we may be able to remove the setters from some values, but that's not crucial. This also moves us towards aspirational goals of storing a JsonNode for the Value alongside the de-serialized object, which would help with bugs such as SeleniumHQ#14846 and many others involving incorrect de-serialization, especially with .ToString() invocations on dictionaries/lists, and as casts when the type is not a match. --------- Co-authored-by: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com>
1 parent 8f079f0 commit 01e98c2

File tree

2 files changed

+59
-51
lines changed

2 files changed

+59
-51
lines changed

dotnet/src/webdriver/Remote/HttpCommandExecutor.cs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ private async Task<HttpResponseInfo> MakeHttpRequest(HttpRequestInfo requestInfo
316316

317317
private Response CreateResponse(HttpResponseInfo responseInfo)
318318
{
319-
Response response = new Response();
319+
Response response;
320320
string body = responseInfo.Body;
321321
if ((int)responseInfo.StatusCode < 200 || (int)responseInfo.StatusCode > 299)
322322
{
@@ -326,8 +326,7 @@ private Response CreateResponse(HttpResponseInfo responseInfo)
326326
}
327327
else
328328
{
329-
response.Status = WebDriverResult.UnknownError;
330-
response.Value = body;
329+
response = new Response(sessionId: null, body, WebDriverResult.UnknownError);
331330
}
332331
}
333332
else if (responseInfo.ContentType != null && responseInfo.ContentType.StartsWith(JsonMimeType, StringComparison.OrdinalIgnoreCase))
@@ -336,12 +335,12 @@ private Response CreateResponse(HttpResponseInfo responseInfo)
336335
}
337336
else
338337
{
339-
response.Value = body;
338+
response = new Response(sessionId: null, body, WebDriverResult.Success);
340339
}
341340

342-
if (response.Value is string)
341+
if (response.Value is string valueString)
343342
{
344-
response.Value = ((string)response.Value).Replace("\r\n", "\n").Replace("\n", Environment.NewLine);
343+
response.Value = valueString.Replace("\r\n", "\n").Replace("\n", Environment.NewLine);
345344
}
346345

347346
return response;

dotnet/src/webdriver/Response.cs

Lines changed: 54 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
using System.Text.Json;
2525
using System.Text.Json.Serialization;
2626

27+
#nullable enable
28+
2729
namespace OpenQA.Selenium
2830
{
2931
/// <summary>
@@ -48,88 +50,98 @@ public Response()
4850
/// Initializes a new instance of the <see cref="Response"/> class
4951
/// </summary>
5052
/// <param name="sessionId">Session ID in use</param>
51-
public Response(SessionId sessionId)
53+
public Response(SessionId? sessionId)
5254
{
53-
if (sessionId != null)
54-
{
55-
this.SessionId = sessionId.ToString();
56-
}
55+
this.SessionId = sessionId?.ToString();
56+
}
57+
58+
/// <summary>
59+
/// Initializes a new instance of the <see cref="Response"/> class
60+
/// </summary>
61+
/// <param name="sessionId">The Session ID in use, if any.</param>
62+
/// <param name="value">The JSON payload of the response.</param>
63+
/// <param name="status">The WebDriver result status of the response.</param>
64+
public Response(string? sessionId, object? value, WebDriverResult status)
65+
{
66+
this.SessionId = sessionId;
67+
this.Value = value;
68+
this.Status = status;
5769
}
5870

5971
/// <summary>
6072
/// Returns a new <see cref="Response"/> from a JSON-encoded string.
6173
/// </summary>
6274
/// <param name="value">The JSON string to deserialize into a <see cref="Response"/>.</param>
6375
/// <returns>A <see cref="Response"/> object described by the JSON string.</returns>
76+
/// <exception cref="ArgumentNullException">If <paramref name="value"/> is <see langword="null"/>.</exception>
77+
/// <exception cref="JsonException">If <paramref name="value"/> is not a valid JSON object.</exception>
6478
public static Response FromJson(string value)
6579
{
66-
Dictionary<string, object> rawResponse = JsonSerializer.Deserialize<Dictionary<string, object>>(value, s_jsonSerializerOptions)
80+
Dictionary<string, object?> rawResponse = JsonSerializer.Deserialize<Dictionary<string, object?>>(value, s_jsonSerializerOptions)
6781
?? throw new WebDriverException("JSON success response returned \"null\" value");
6882

69-
var response = new Response();
83+
object? contents;
84+
string? sessionId = null;
7085

71-
if (rawResponse.ContainsKey("sessionId"))
86+
if (rawResponse.TryGetValue("sessionId", out object? s) && s is not null)
7287
{
73-
if (rawResponse["sessionId"] != null)
74-
{
75-
response.SessionId = rawResponse["sessionId"].ToString();
76-
}
88+
sessionId = s.ToString();
7789
}
7890

79-
if (rawResponse.TryGetValue("value", out object valueObj))
91+
if (rawResponse.TryGetValue("value", out object? valueObj))
8092
{
81-
response.Value = valueObj;
93+
contents = valueObj;
8294
}
83-
84-
// If the returned object does *not* have a "value" property
85-
// the response value should be the entirety of the response.
86-
// TODO: Remove this if statement altogether; there should
87-
// never be a spec-compliant response that does not contain a
88-
// value property.
89-
if (!rawResponse.ContainsKey("value") && response.Value == null)
95+
else
9096
{
97+
// If the returned object does *not* have a "value" property
98+
// the response value should be the entirety of the response.
99+
// TODO: Remove this if statement altogether; there should
100+
// never be a spec-compliant response that does not contain a
101+
// value property.
102+
91103
// Special-case for the new session command, where the "capabilities"
92104
// property of the response is the actual value we're interested in.
93-
if (rawResponse.ContainsKey("capabilities"))
105+
if (rawResponse.TryGetValue("capabilities", out object? capabilities))
94106
{
95-
response.Value = rawResponse["capabilities"];
107+
contents = capabilities;
96108
}
97109
else
98110
{
99-
response.Value = rawResponse;
111+
contents = rawResponse;
100112
}
101113
}
102114

103-
if (response.Value is Dictionary<string, object> valueDictionary)
115+
if (contents is Dictionary<string, object?> valueDictionary)
104116
{
105117
// Special case code for the new session command. If the response contains
106118
// sessionId and capabilities properties, fix up the session ID and value members.
107-
if (valueDictionary.ContainsKey("sessionId"))
119+
if (valueDictionary.TryGetValue("sessionId", out object? session))
108120
{
109-
response.SessionId = valueDictionary["sessionId"].ToString();
110-
if (valueDictionary.TryGetValue("capabilities", out object capabilities))
121+
sessionId = session.ToString();
122+
if (valueDictionary.TryGetValue("capabilities", out object? capabilities))
111123
{
112-
response.Value = capabilities;
124+
contents = capabilities;
113125
}
114126
else
115127
{
116-
response.Value = valueDictionary["value"];
128+
contents = valueDictionary["value"];
117129
}
118130
}
119131
}
120132

121-
return response;
133+
return new Response(sessionId, contents, WebDriverResult.Success);
122134
}
123135

124136
/// <summary>
125137
/// Gets or sets the value from JSON.
126138
/// </summary>
127-
public object Value { get; set; }
139+
public object? Value { get; set; }
128140

129141
/// <summary>
130142
/// Gets or sets the session ID.
131143
/// </summary>
132-
public string SessionId { get; set; }
144+
public string? SessionId { get; set; }
133145

134146
/// <summary>
135147
/// Gets or sets the status value of the response.
@@ -142,26 +154,25 @@ public static Response FromJson(string value)
142154
/// </summary>
143155
/// <param name="value">The JSON string to deserialize into a <see cref="Response"/>.</param>
144156
/// <returns>A <see cref="Response"/> object described by the JSON string.</returns>
157+
/// <exception cref="ArgumentNullException">If <paramref name="value"/> is <see langword="null"/>.</exception>
158+
/// <exception cref="JsonException">If <paramref name="value"/> is not a valid JSON object.</exception>
159+
/// <exception cref="WebDriverException">If the JSON dictionary is not in the expected state, per spec.</exception>
145160
public static Response FromErrorJson(string value)
146161
{
147-
var deserializedResponse = JsonSerializer.Deserialize<Dictionary<string, object>>(value, s_jsonSerializerOptions)
162+
Dictionary<string, object?> deserializedResponse = JsonSerializer.Deserialize<Dictionary<string, object?>>(value, s_jsonSerializerOptions)
148163
?? throw new WebDriverException("JSON error response returned \"null\" value");
149164

150-
var response = new Response();
151-
152-
if (!deserializedResponse.TryGetValue("value", out var valueObject))
165+
if (!deserializedResponse.TryGetValue("value", out object? valueObject))
153166
{
154167
throw new WebDriverException($"The 'value' property was not found in the response:{Environment.NewLine}{value}");
155168
}
156169

157-
if (valueObject is not Dictionary<string, object> valueDictionary)
170+
if (valueObject is not Dictionary<string, object?> valueDictionary)
158171
{
159172
throw new WebDriverException($"The 'value' property is not a dictionary of <string, object>{Environment.NewLine}{value}");
160173
}
161174

162-
response.Value = valueDictionary;
163-
164-
if (!valueDictionary.TryGetValue("error", out var errorObject))
175+
if (!valueDictionary.TryGetValue("error", out object? errorObject))
165176
{
166177
throw new WebDriverException($"The 'value > error' property was not found in the response:{Environment.NewLine}{value}");
167178
}
@@ -171,11 +182,9 @@ public static Response FromErrorJson(string value)
171182
throw new WebDriverException($"The 'value > error' property is not a string{Environment.NewLine}{value}");
172183
}
173184

174-
response.Value = deserializedResponse["value"];
175-
176-
response.Status = WebDriverError.ResultFromError(errorString);
185+
WebDriverResult status = WebDriverError.ResultFromError(errorString);
177186

178-
return response;
187+
return new Response(sessionId: null, valueDictionary, status);
179188
}
180189

181190
/// <summary>

0 commit comments

Comments
 (0)