Skip to content

Commit bbc6d3e

Browse files
authored
Fixed graphql-transport-ws implementation (#528)
* fixing ws subprotocol impl: - Data should be in the property `data`, and errors should be in the property `errors` - "Error" is only intended as a response for "Subscribe" and, of course, includes any exception thrown during processing of "Subscribe". * AspNetCore startup ext.: fixing default websocket endpoint path It was not being set because of the calls in method overloads. It was explicitly being set to `null`. * `webSocketEndpointUrl` -> `webSocketEndpointPath` Because it's a path and not a URL :)
1 parent 9e33deb commit bbc6d3e

File tree

3 files changed

+54
-53
lines changed

3 files changed

+54
-53
lines changed

src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs

Lines changed: 29 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -169,33 +169,27 @@ type GraphQLWebSocketMiddleware<'Root>
169169
let sendMsg = sendMessageViaSocket serializerOptions socket
170170
let rcv () = socket |> rcvMsgViaSocket serializerOptions
171171

172-
let sendOutput id (output : Output) =
173-
match output.TryGetValue "errors" with
174-
| true, theValue ->
175-
// The specification says: "This message terminates the operation and no further messages will be sent."
176-
subscriptions
177-
|> GraphQLSubscriptionsManagement.removeSubscription (id)
178-
sendMsg (Error (id, unbox theValue))
179-
| false, _ -> sendMsg (Next (id, output))
172+
let sendOutput id (output : SubscriptionExecutionResult) =
173+
sendMsg (Next (id, output))
180174

181175
let sendSubscriptionResponseOutput id subscriptionResult =
182176
match subscriptionResult with
183-
| SubscriptionResult output -> output |> sendOutput id
177+
| SubscriptionResult output -> { Data = ValueSome output; Errors = [] } |> sendOutput id
184178
| SubscriptionErrors (output, errors) ->
185179
logger.LogWarning ("Subscription errors: {subscriptionErrors}", (String.Join ('\n', errors |> Seq.map (fun x -> $"- %s{x.Message}"))))
186-
Task.FromResult ()
180+
{ Data = ValueNone; Errors = errors } |> sendOutput id
187181

188182
let sendDeferredResponseOutput id deferredResult =
189183
match deferredResult with
190184
| DeferredResult (obj, path) ->
191185
let output = obj :?> Dictionary<string, obj>
192-
output |> sendOutput id
186+
{ Data = ValueSome output; Errors = [] } |> sendOutput id
193187
| DeferredErrors (obj, errors, _) ->
194188
logger.LogWarning (
195189
"Deferred response errors: {deferredErrors}",
196190
(String.Join ('\n', errors |> Seq.map (fun x -> $"- %s{x.Message}")))
197191
)
198-
Task.FromResult ()
192+
{ Data = ValueNone; Errors = errors } |> sendOutput id
199193

200194
let sendDeferredResultDelayedBy (ct : CancellationToken) (ms : int) id deferredResult : Task = task {
201195
do! Task.Delay (ms, ct)
@@ -208,16 +202,16 @@ type GraphQLWebSocketMiddleware<'Root>
208202
(subscriptions, socket, observableOutput, serializerOptions)
209203
|> addClientSubscription id sendSubscriptionResponseOutput
210204
| Deferred (data, errors, observableOutput) ->
211-
do! data |> sendOutput id
205+
do! { Data = ValueSome data; Errors = [] } |> sendOutput id
212206
if errors.IsEmpty then
213207
(subscriptions, socket, observableOutput, serializerOptions)
214208
|> addClientSubscription id (sendDeferredResultDelayedBy cancellationToken 5000)
215209
else
216210
()
217-
| Direct (data, _) -> do! data |> sendOutput id
211+
| Direct (data, _) -> do! { Data = ValueSome data; Errors = [] } |> sendOutput id
218212
| RequestError problemDetails ->
219213
logger.LogWarning("Request errors:\n{errors}", problemDetails)
220-
214+
do! { Data = ValueNone; Errors = problemDetails } |> sendOutput id
221215
}
222216

223217
let logMsgReceivedWithOptionalPayload optionalPayload (msgAsStr : string) =
@@ -265,22 +259,26 @@ type GraphQLWebSocketMiddleware<'Root>
265259
| ValueNone -> do! ServerPong p |> sendMsg
266260
| ClientPong p -> nameof ClientPong |> logMsgReceivedWithOptionalPayload p
267261
| Subscribe (id, query) ->
268-
nameof Subscribe |> logMsgWithIdReceived id
269-
if subscriptions |> GraphQLSubscriptionsManagement.isIdTaken id then
270-
do!
271-
let warningMsg : FormattableString = $"Subscriber for Id = '{id}' already exists"
272-
logger.LogWarning (String.Format (warningMsg.Format, "id"), id)
273-
socket.CloseAsync (
274-
enum CustomWebSocketStatus.SubscriberAlreadyExists,
275-
warningMsg.ToString (),
276-
CancellationToken.None
277-
)
278-
else
279-
let variables = query.Variables |> Skippable.toOption
280-
let! planExecutionResult =
281-
let root = options.RootFactory httpContext
282-
options.SchemaExecutor.AsyncExecute (query.Query, root, ?variables = variables)
283-
do! planExecutionResult |> applyPlanExecutionResult id socket
262+
try
263+
nameof Subscribe |> logMsgWithIdReceived id
264+
if subscriptions |> GraphQLSubscriptionsManagement.isIdTaken id then
265+
do!
266+
let warningMsg : FormattableString = $"Subscriber for Id = '{id}' already exists"
267+
logger.LogWarning (String.Format (warningMsg.Format, "id"), id)
268+
socket.CloseAsync (
269+
enum CustomWebSocketStatus.SubscriberAlreadyExists,
270+
warningMsg.ToString (),
271+
CancellationToken.None
272+
)
273+
else
274+
let variables = query.Variables |> Skippable.toOption
275+
let! planExecutionResult =
276+
let root = options.RootFactory httpContext
277+
options.SchemaExecutor.AsyncExecute (query.Query, root, ?variables = variables)
278+
do! planExecutionResult |> applyPlanExecutionResult id socket
279+
with ex ->
280+
logger.LogError (ex, "Unexpected error during subscription with id '{id}'", id)
281+
do! sendMsg (Error (id, [new Shared.NameValueLookup ([ ("subscription", "Unexpected error during subscription" :> obj) ])]))
284282
| ClientComplete id ->
285283
"ClientComplete" |> logMsgWithIdReceived id
286284
subscriptions

src/FSharp.Data.GraphQL.Server.AspNetCore/StartupExtensions.fs

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ module ServiceCollectionExtensions =
4545
executorFactory : Func<IServiceProvider, Executor<'Root>>,
4646
rootFactory : HttpContext -> 'Root,
4747
[<Optional>] additionalConverters : JsonConverter seq,
48-
[<Optional; DefaultParameterValue (GraphQLOptionsDefaults.WebSocketEndpoint)>] webSocketEndpointUrl : string,
48+
[<Optional; DefaultParameterValue (GraphQLOptionsDefaults.WebSocketEndpoint)>] webSocketEndpointPath : string,
4949
[<Optional>] configure : Func<GraphQLOptions<'Root>, GraphQLOptions<'Root>>
5050
) =
5151

@@ -56,7 +56,7 @@ module ServiceCollectionExtensions =
5656

5757
let getOptions sp =
5858
let executor = executorFactory.Invoke sp
59-
let options = createStandardOptions executor rootFactory additionalConverters webSocketEndpointUrl
59+
let options = createStandardOptions executor rootFactory additionalConverters webSocketEndpointPath
6060
match configure with
6161
| null -> options
6262
| _ -> configure.Invoke options
@@ -99,10 +99,10 @@ module ServiceCollectionExtensions =
9999
executorFactory : Func<IServiceProvider, Executor<'Root>>,
100100
rootFactory : HttpContext -> 'Root,
101101
[<Optional>] additionalConverters : JsonConverter seq,
102-
[<Optional; DefaultParameterValue (GraphQLOptionsDefaults.WebSocketEndpoint)>] webSocketEndpointUrl : string,
102+
[<Optional; DefaultParameterValue (GraphQLOptionsDefaults.WebSocketEndpoint)>] webSocketEndpointPath : string,
103103
[<Optional>] configure : Func<GraphQLOptions<'Root>, GraphQLOptions<'Root>>
104104
) =
105-
services.AddGraphQL<'Root, DefaultGraphQLRequestHandler<'Root>> (executorFactory, rootFactory, additionalConverters, webSocketEndpointUrl, configure)
105+
services.AddGraphQL<'Root, DefaultGraphQLRequestHandler<'Root>> (executorFactory, rootFactory, additionalConverters, webSocketEndpointPath, configure)
106106

107107
/// <summary>
108108
/// Adds GraphQL options and services to the service collection. Requires an executor instance to be provided.
@@ -118,7 +118,7 @@ module ServiceCollectionExtensions =
118118
rootFactory : HttpContext -> 'Root,
119119
[<Optional>] additionalConverters : JsonConverter seq
120120
) =
121-
services.AddGraphQL<'Root, 'Handler> ((fun _ -> executor), rootFactory, additionalConverters, null, null)
121+
services.AddGraphQL<'Root, 'Handler> ((fun _ -> executor), rootFactory, additionalConverters, configure = null)
122122

123123
/// <summary>
124124
/// Adds GraphQL options and services to the service collection. Requires an executor instance to be provided.
@@ -134,7 +134,7 @@ module ServiceCollectionExtensions =
134134
rootFactory : HttpContext -> 'Root,
135135
[<Optional>] additionalConverters : JsonConverter seq
136136
) =
137-
services.AddGraphQL<'Root> ((fun _ -> executor), rootFactory, additionalConverters, null, null)
137+
services.AddGraphQL<'Root> ((fun _ -> executor), rootFactory, additionalConverters, configure = null)
138138

139139
/// <summary>
140140
/// Adds GraphQL options and services to the service collection. Requires an executor instance to be provided.
@@ -148,10 +148,10 @@ module ServiceCollectionExtensions =
148148
(
149149
executor : Executor<'Root>,
150150
rootFactory : HttpContext -> 'Root,
151-
webSocketEndpointUrl : string,
151+
webSocketEndpointPath : string,
152152
[<Optional>] additionalConverters : JsonConverter seq
153153
) =
154-
services.AddGraphQL<'Root, 'Handler> ((fun _ -> executor), rootFactory, additionalConverters, webSocketEndpointUrl, null)
154+
services.AddGraphQL<'Root, 'Handler> ((fun _ -> executor), rootFactory, additionalConverters, webSocketEndpointPath, null)
155155

156156
/// <summary>
157157
/// Adds GraphQL options and services to the service collection. Requires an executor instance to be provided.
@@ -165,10 +165,10 @@ module ServiceCollectionExtensions =
165165
(
166166
executor : Executor<'Root>,
167167
rootFactory : HttpContext -> 'Root,
168-
webSocketEndpointUrl : string,
168+
webSocketEndpointPath : string,
169169
[<Optional>] additionalConverters : JsonConverter seq
170170
) =
171-
services.AddGraphQL<'Root> ((fun _ -> executor), rootFactory, additionalConverters, webSocketEndpointUrl, null)
171+
services.AddGraphQL<'Root> ((fun _ -> executor), rootFactory, additionalConverters, webSocketEndpointPath, null)
172172

173173
/// <summary>
174174
/// Adds GraphQL options and services to the service collection. Requires an executor instance to be provided.
@@ -185,7 +185,7 @@ module ServiceCollectionExtensions =
185185
configure : Func<GraphQLOptions<'Root>, GraphQLOptions<'Root>>,
186186
[<Optional>] additionalConverters : JsonConverter seq
187187
) =
188-
services.AddGraphQL<'Root, 'Handler> ((fun _ -> executor), rootFactory, additionalConverters, null, configure)
188+
services.AddGraphQL<'Root, 'Handler> ((fun _ -> executor), rootFactory, additionalConverters, configure = configure)
189189

190190
/// <summary>
191191
/// Adds GraphQL options and services to the service collection. Requires an executor instance to be provided.
@@ -202,7 +202,7 @@ module ServiceCollectionExtensions =
202202
configure : Func<GraphQLOptions<'Root>, GraphQLOptions<'Root>>,
203203
[<Optional>] additionalConverters : JsonConverter seq
204204
) =
205-
services.AddGraphQL<'Root> ((fun _ -> executor), rootFactory, additionalConverters, null, configure)
205+
services.AddGraphQL<'Root> ((fun _ -> executor), rootFactory, additionalConverters, configure = configure)
206206

207207
/// <summary>
208208
/// Adds GraphQL options and services to the service collection. It gets the executor from the service provider.
@@ -221,7 +221,7 @@ module ServiceCollectionExtensions =
221221
[<Optional>] additionalConverters : JsonConverter seq
222222
) =
223223
let getExecutorService (sp : IServiceProvider) = sp.GetRequiredService<Executor<'Root>>()
224-
services.AddGraphQL<'Root, 'Handler> (getExecutorService, rootFactory, additionalConverters, null, null)
224+
services.AddGraphQL<'Root, 'Handler> (getExecutorService, rootFactory, additionalConverters, configure = null)
225225

226226
/// <summary>
227227
/// Adds GraphQL options and services to the service collection. It gets the executor from the service provider.
@@ -240,7 +240,7 @@ module ServiceCollectionExtensions =
240240
[<Optional>] additionalConverters : JsonConverter seq
241241
) =
242242
let getExecutorService (sp : IServiceProvider) = sp.GetRequiredService<Executor<'Root>>()
243-
services.AddGraphQL<'Root> (getExecutorService, rootFactory, additionalConverters, null, null)
243+
services.AddGraphQL<'Root> (getExecutorService, rootFactory, additionalConverters, configure = null)
244244

245245
/// <summary>
246246
/// Adds GraphQL options and services to the service collection. It gets the executor from the service provider.
@@ -256,11 +256,11 @@ module ServiceCollectionExtensions =
256256
member services.AddGraphQL<'Root, 'Handler when 'Handler :> GraphQLRequestHandler<'Root> and 'Handler : not struct>
257257
(
258258
rootFactory : HttpContext -> 'Root,
259-
[<Optional; DefaultParameterValue (GraphQLOptionsDefaults.WebSocketEndpoint)>] webSocketEndpointUrl : string,
259+
[<Optional; DefaultParameterValue (GraphQLOptionsDefaults.WebSocketEndpoint)>] webSocketEndpointPath : string,
260260
[<Optional>] additionalConverters : JsonConverter seq
261261
) =
262262
let getExecutorService (sp : IServiceProvider) = sp.GetRequiredService<Executor<'Root>>()
263-
services.AddGraphQL<'Root, 'Handler> (getExecutorService, rootFactory, additionalConverters, webSocketEndpointUrl, null)
263+
services.AddGraphQL<'Root, 'Handler> (getExecutorService, rootFactory, additionalConverters, webSocketEndpointPath, null)
264264

265265
/// <summary>
266266
/// Adds GraphQL options and services to the service collection. It gets the executor from the service provider.
@@ -276,11 +276,11 @@ module ServiceCollectionExtensions =
276276
member services.AddGraphQL<'Root>
277277
(
278278
rootFactory : HttpContext -> 'Root,
279-
[<Optional; DefaultParameterValue (GraphQLOptionsDefaults.WebSocketEndpoint)>] webSocketEndpointUrl : string,
279+
[<Optional; DefaultParameterValue (GraphQLOptionsDefaults.WebSocketEndpoint)>] webSocketEndpointPath : string,
280280
[<Optional>] additionalConverters : JsonConverter seq
281281
) =
282282
let getExecutorService (sp : IServiceProvider) = sp.GetRequiredService<Executor<'Root>>()
283-
services.AddGraphQL<'Root> (getExecutorService, rootFactory, additionalConverters, webSocketEndpointUrl, null)
283+
services.AddGraphQL<'Root> (getExecutorService, rootFactory, additionalConverters, webSocketEndpointPath, null)
284284

285285
/// <summary>
286286
/// Adds GraphQL options and services to the service collection. It gets the executor from the service provider.
@@ -300,7 +300,7 @@ module ServiceCollectionExtensions =
300300
[<Optional>] additionalConverters : JsonConverter seq
301301
) =
302302
let getExecutorService (sp : IServiceProvider) = sp.GetRequiredService<Executor<'Root>>()
303-
services.AddGraphQL<'Root, 'Handler> (getExecutorService, rootFactory, additionalConverters, null, configure)
303+
services.AddGraphQL<'Root, 'Handler> (getExecutorService, rootFactory, additionalConverters, configure = null)
304304

305305
/// <summary>
306306
/// Adds GraphQL options and services to the service collection. It gets the executor from the service provider.
@@ -320,7 +320,7 @@ module ServiceCollectionExtensions =
320320
[<Optional>] additionalConverters : JsonConverter seq
321321
) =
322322
let getExecutorService (sp : IServiceProvider) = sp.GetRequiredService<Executor<'Root>>()
323-
services.AddGraphQL<'Root> (getExecutorService, rootFactory, additionalConverters, null, configure)
323+
services.AddGraphQL<'Root> (getExecutorService, rootFactory, additionalConverters, configure = configure)
324324

325325

326326
[<AutoOpen; Extension>]

src/FSharp.Data.GraphQL.Shared/WebSockets.fs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ namespace FSharp.Data.GraphQL.Shared.WebSockets
33
open System
44
open System.Collections.Generic
55
open System.Text.Json
6+
open FSharp.Data.GraphQL
67
open FSharp.Data.GraphQL.Shared
78

89
type InvalidWebsocketMessageException (explanation : string) =
@@ -15,8 +16,10 @@ type SubscriptionsDict = IDictionary<SubscriptionId, SubscriptionUnsubscriber *
1516

1617
type RawMessage = { Id : string voption; Type : string; Payload : JsonDocument voption }
1718

19+
type SubscriptionExecutionResult = { Data : Output voption; Errors : GQLProblemDetails list }
20+
1821
type ServerRawPayload =
19-
| ExecutionResult of Output
22+
| ExecutionResult of SubscriptionExecutionResult
2023
| ErrorMessages of NameValueLookup list
2124
| CustomResponse of JsonDocument
2225

@@ -35,7 +38,7 @@ type ServerMessage =
3538
| ConnectionAck
3639
| ServerPing
3740
| ServerPong of JsonDocument voption
38-
| Next of id : string * payload : Output
41+
| Next of id : string * payload : SubscriptionExecutionResult
3942
| Error of id : string * err : NameValueLookup list
4043
| Complete of id : string
4144

0 commit comments

Comments
 (0)