Skip to content

feat: implement websocket subprotocols for subscriptions graphql-ws, graphql-transport-ws - #21

Open
pankgeorg wants to merge 11 commits into
DeloitteOptimalReality:mainfrom
pankgeorg:pg/http_1_fixed_subscriptions
Open

feat: implement websocket subprotocols for subscriptions graphql-ws, graphql-transport-ws#21
pankgeorg wants to merge 11 commits into
DeloitteOptimalReality:mainfrom
pankgeorg:pg/http_1_fixed_subscriptions

Conversation

@pankgeorg

@pankgeorgpankgeorg commented Oct 14, 2022

Copy link
Copy Markdown
Contributor

Implements https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md and the newer and currently maintained https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md

WebSocket subscription works on Hasura 2.14.0-beta-1 with this patch, using both protocols.

One can choose protocol (sub-protocol) by using the kwargument to open_subscription: websocket_protocol. By default this is set to "graphql-ws, graphql-transport-ws" which leaves the decision up to the server. It can be overriden with any of the two values; GraphQLClient.PROTOCOL_GRAPHQL_WS or GraphQLClient.PROTOCOL_APOLLO_OLD. In that case, the client will explicitly request one protocol (and the server should comply - if it doesn't we adapt).

Includes #19 so le'ts put that in first.

pankgeorgand others added 9 commits July 17, 2022 16:06
@pankgeorg
pankgeorgforce-pushed the pg/http_1_fixed_subscriptions branch 2 times, most recently from d761bb9 to 504992cCompareOctober 14, 2022 11:36
@pankgeorg
pankgeorgforce-pushed the pg/http_1_fixed_subscriptions branch from 504992c to d9af5c5CompareOctober 14, 2022 11:37
@codecov-commenter

codecov-commenter commented Oct 14, 2022

Copy link
Copy Markdown

Codecov Report

Merging #21 (ad669c8) into main (d6e6ac2) will decrease coverage by 10.15%.
The diff coverage is 23.57%.

@@ Coverage Diff @@## main #21 +/- ##
===========================================
- Coverage 67.27% 57.12% -10.16% 
===========================================
Files 17 18 +1 Lines 602 702 +100 ===========================================
- Hits 405 401 -4 - Misses 197 301 +104 
Impacted FilesCoverage Δ
src/GraphQLClient.jl100.00% <ø> (ø)
src/ws_subscription_protocols.jl10.81% <10.81%> (ø)
src/subscriptions.jl80.82% <71.42%> (-8.47%)⬇️
src/gqlresponse.jl24.32% <100.00%> (ø)

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

@pankgeorg

pankgeorg commented Oct 16, 2022

Copy link
Copy Markdown
ContributorAuthor

On version 0.7.5 opening a subscription to a Hasura 2.14.0-beta-ce server retrurns

`Cannot `convert` an object of type Nothing to an object of type String`
julia> open_subscription(client, "jobs", output_fields=["id"], verbose=2) do r
@info "got results"
return false
end
Starting jobs subscription with ID 1-1
Listening to jobs with ID 1-1...
ERROR: MethodError: Cannot `convert` an object of type Nothing to an object of type String
Closest candidates are:
convert(::Type{String}, ::String) at essentials.jl:218
convert(::Type{T}, ::T) where T<:AbstractString at strings/basic.jl:231
convert(::Type{T}, ::AbstractString) where T<:AbstractString at strings/basic.jl:232
...
Stacktrace:
[1] GraphQLClient.GQLSubscriptionResponse{Any}(id::Nothing, type::String, payload::Nothing)
@ GraphQLClient ~/GraphQLClient.jl/src/gqlresponse.jl:102
[2] construct
@ ~/.julia/packages/StructTypes/AK4aM/src/StructTypes.jl:912 [inlined]
[3] read(::StructTypes.UnorderedStruct, buf::Vector{UInt8}, pos::Int64, len::Int64, b::UInt8, ::Type{GraphQLClient.GQLSubscriptionResponse{Any}}; kw::Base.Pairs{Symbol, Union{}, Tuple{}, NamedTuple{(), Tuple{}}})
@ JSON3 ~/.julia/packages/JSON3/vguNZ/src/structs.jl:645
[4] read
@ ~/.julia/packages/JSON3/vguNZ/src/structs.jl:566 [inlined]
[5] read(str::JSON3.VectorString{Vector{UInt8}}, ::Type{GraphQLClient.GQLSubscriptionResponse{Any}}; jsonlines::Bool, kw::Base.Pairs{Symbol, Union{}, Tuple{}, NamedTuple{(), Tuple{}}})
@ JSON3 ~/.julia/packages/JSON3/vguNZ/src/structs.jl:40
[6] read
@ ~/.julia/packages/JSON3/vguNZ/src/structs.jl:32 [inlined]
[7] #read#15
@ ~/.julia/packages/JSON3/vguNZ/src/structs.jl:14 [inlined]
[8] read(bytes::Vector{UInt8}, ::Type{GraphQLClient.GQLSubscriptionResponse{Any}})
@ JSON3 ~/.julia/packages/JSON3/vguNZ/src/structs.jl:14
[9] (::GraphQLClient.var"#35#36"{Nothing, Int64, Nothing, Bool, Int64, var"#1#2", String, DataType, String})(ws::HTTP.WebSockets.WebSocket{HTTP.ConnectionPool.Transaction{Sockets.TCPSocket}})
@ GraphQLClient ~/GraphQLClient.jl/src/subscriptions.jl:123
[10] (::HTTP.WebSockets.var"#3#4"{Bool, GraphQLClient.var"#35#36"{Nothing, Int64, Nothing, Bool, Int64, var"#1#2", String, DataType, String}, String})(http::HTTP.Streams.Stream{HTTP.Messages.Response, HTTP.ConnectionPool.Transaction{Sockets.TCPSocket}})
@ HTTP.WebSockets ~/.julia/packages/HTTP/aTjcj/src/WebSockets.jl:124
[11] macro expansion
@ ~/.julia/packages/HTTP/aTjcj/src/StreamRequest.jl:70 [inlined]
[12] macro expansion
@ ./task.jl:454 [inlined]
[13] request(::Type{HTTP.StreamRequest.StreamLayer{Union{}}}, io::HTTP.ConnectionPool.Transaction{Sockets.TCPSocket}, req::HTTP.Messages.Request, body::Nothing; reached_redirect_limit::Bool, response_stream::Nothing, iofunction::HTTP.WebSockets.var"#3#4"{Bool, GraphQLClient.var"#35#36"{Nothing, Int64, Nothing, Bool, Int64, var"#1#2", String, DataType, String}, String}, verbose::Int64, kw::Base.Pairs{Symbol, Bool, Tuple{Symbol}, NamedTuple{(:retry,), Tuple{Bool}}})
@ HTTP.StreamRequest ~/.julia/packages/HTTP/aTjcj/src/StreamRequest.jl:57
[14] request(::Type{HTTP.ConnectionRequest.ConnectionPoolLayer{HTTP.StreamRequest.StreamLayer{Union{}}}}, url::URIs.URI, req::HTTP.Messages.Request, body::Nothing; proxy::Nothing, socket_type::Type, reuse_limit::Int64, kw::Base.Pairs{Symbol, Any, NTuple{4, Symbol}, NamedTuple{(:iofunction, :reached_redirect_limit, :verbose, :retry), Tuple{HTTP.WebSockets.var"#3#4"{Bool, GraphQLClient.var"#35#36"{Nothing, Int64, Nothing, Bool, Int64, var"#1#2", String, DataType, String}, String}, Bool, Int64, Bool}}})
@ HTTP.ConnectionRequest ~/.julia/packages/HTTP/aTjcj/src/ConnectionRequest.jl:108
[15] request(::Type{HTTP.ExceptionRequest.ExceptionLayer{HTTP.ConnectionRequest.ConnectionPoolLayer{HTTP.StreamRequest.StreamLayer{Union{}}}}}, ::URIs.URI, ::Vararg{Any}; kw::Base.Pairs{Symbol, Any, NTuple{5, Symbol}, NamedTuple{(:iofunction, :reached_redirect_limit, :reuse_limit, :verbose, :retry), Tuple{HTTP.WebSockets.var"#3#4"{Bool, GraphQLClient.var"#35#36"{Nothing, Int64, Nothing, Bool, Int64, var"#1#2", String, DataType, String}, String}, Bool, Int64, Int64, Bool}}})
@ HTTP.ExceptionRequest ~/.julia/packages/HTTP/aTjcj/src/ExceptionRequest.jl:19
[16] (::Base.var"#88#90"{Base.var"#88#89#91"{ExponentialBackOff, HTTP.RetryRequest.var"#2#3"{Bool, HTTP.Messages.Request}, typeof(HTTP.request)}})(::Type, ::Vararg{Any}; kwargs::Base.Pairs{Symbol, Any, NTuple{5, Symbol}, NamedTuple{(:iofunction, :reached_redirect_limit, :reuse_limit, :verbose, :retry), Tuple{HTTP.WebSockets.var"#3#4"{Bool, GraphQLClient.var"#35#36"{Nothing, Int64, Nothing, Bool, Int64, var"#1#2", String, DataType, String}, String}, Bool, Int64, Int64, Bool}}})
@ Base ./error.jl:296
[17] #request#1
@ ~/.julia/packages/HTTP/aTjcj/src/RetryRequest.jl:44 [inlined]
[18] request(::Type{HTTP.MessageRequest.MessageLayer{HTTP.RetryRequest.RetryLayer{HTTP.ExceptionRequest.ExceptionLayer{HTTP.ConnectionRequest.ConnectionPoolLayer{HTTP.StreamRequest.StreamLayer{Union{}}}}}}}, method::String, url::URIs.URI, headers::Vector{Pair{SubString{String}, SubString{String}}}, body::Nothing; http_version::VersionNumber, target::String, parent::Nothing, iofunction::Function, kw::Base.Pairs{Symbol, Integer, NTuple{4, Symbol}, NamedTuple{(:reached_redirect_limit, :reuse_limit, :verbose, :retry), Tuple{Bool, Int64, Int64, Bool}}})
@ HTTP.MessageRequest ~/.julia/packages/HTTP/aTjcj/src/MessageRequest.jl:66
[19] request(::Type{HTTP.BasicAuthRequest.BasicAuthLayer{HTTP.MessageRequest.MessageLayer{HTTP.RetryRequest.RetryLayer{HTTP.ExceptionRequest.ExceptionLayer{HTTP.ConnectionRequest.ConnectionPoolLayer{HTTP.StreamRequest.StreamLayer{Union{}}}}}}}}, method::String, url::URIs.URI, headers::Vector{Pair{SubString{String}, SubString{String}}}, body::Nothing; kw::Base.Pairs{Symbol, Any, NTuple{5, Symbol}, NamedTuple{(:reached_redirect_limit, :iofunction, :reuse_limit, :verbose, :retry), Tuple{Bool, HTTP.WebSockets.var"#3#4"{Bool, GraphQLClient.var"#35#36"{Nothing, Int64, Nothing, Bool, Int64, var"#1#2", String, DataType, String}, String}, Int64, Int64, Bool}}})
@ HTTP.BasicAuthRequest ~/.julia/packages/HTTP/aTjcj/src/BasicAuthRequest.jl:28
[20] request(::Type{HTTP.RedirectRequest.RedirectLayer{HTTP.BasicAuthRequest.BasicAuthLayer{HTTP.MessageRequest.MessageLayer{HTTP.RetryRequest.RetryLayer{HTTP.ExceptionRequest.ExceptionLayer{HTTP.ConnectionRequest.ConnectionPoolLayer{HTTP.StreamRequest.StreamLayer{Union{}}}}}}}}}, method::String, url::URIs.URI, headers::Vector{Pair{SubString{String}, SubString{String}}}, body::Nothing; redirect_limit::Int64, forwardheaders::Bool, kw::Base.Pairs{Symbol, Any, NTuple{4, Symbol}, NamedTuple{(:iofunction, :reuse_limit, :verbose, :retry), Tuple{HTTP.WebSockets.var"#3#4"{Bool, GraphQLClient.var"#35#36"{Nothing, Int64, Nothing, Bool, Int64, var"#1#2", String, DataType, String}, String}, Int64, Int64, Bool}}})
@ HTTP.RedirectRequest ~/.julia/packages/HTTP/aTjcj/src/RedirectRequest.jl:28
[21] #request#1
@ ~/.julia/packages/HTTP/aTjcj/src/TopRequest.jl:15 [inlined]
[22] request(method::String, url::String, h::Vector{Pair{String, String}}, b::Nothing; headers::Vector{Pair{String, String}}, body::Nothing, query::Nothing, kw::Base.Pairs{Symbol, Any, NTuple{4, Symbol}, NamedTuple{(:iofunction, :reuse_limit, :verbose, :retry), Tuple{HTTP.WebSockets.var"#3#4"{Bool, GraphQLClient.var"#35#36"{Nothing, Int64, Nothing, Bool, Int64, var"#1#2", String, DataType, String}, String}, Int64, Int64, Bool}}})
@ HTTP ~/.julia/packages/HTTP/aTjcj/src/HTTP.jl:330
[23] #open#11
@ ~/.julia/packages/HTTP/aTjcj/src/HTTP.jl:364 [inlined]
[24] open(f::GraphQLClient.var"#35#36"{Nothing, Int64, Nothing, Bool, Int64, var"#1#2", String, DataType, String}, url::String; binary::Bool, verbose::Bool, headers::Dict{String, String}, kw::Base.Pairs{Symbol, Bool, Tuple{Symbol}, NamedTuple{(:retry,), Tuple{Bool}}})
@ HTTP.WebSockets ~/.julia/packages/HTTP/aTjcj/src/WebSockets.jl:104
[25] open_subscription(fn::var"#1#2", client::Client, subscription_name::String, output_type::Type; sub_args::Dict{Any, Any}, output_fields::Vector{String}, initfn::Nothing, retry::Bool, subtimeout::Int64, stopfn::Nothing, throw_on_execution_error::Bool, verbose::Int64)
@ GraphQLClient ~/GraphQLClient.jl/src/subscriptions.jl:97
[26] top-level scope
@ REPL[11]:1

That happens because the server sends a keepalive message that doesn't have all the fields defined in src/gqlresponse.jl:101:

{"type": "ka"}

struct GQLSubscriptionResponse{T}
id::String
type::String
payload::GQLResponse{T}
end
The simple patch to that complains about not initializing the connection properly

image

where the solution - at least for this server - is to align the protocols.

I'm looking for a new way to also fix the tests of the subscriptions using a server in GitHub actions.

@pankgeorgpankgeorg changed the title feat: implement apollographql/subscriptions-transport-ws protocolfeat: implement websocket subprotocols for subscriptions graphql-ws, graphql-transport-wsOct 17, 2022
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@pankgeorg@codecov-commenter