Skip to content

Commit

Permalink
feat: add more WebSocket options for subscription client (#126)
Browse files Browse the repository at this point in the history
* add more WebSocket options for subscription client
* add a more graphql-ws example
  • Loading branch information
hgiasac committed Feb 3, 2024
1 parent 3d72b42 commit 8f1d49e
Show file tree
Hide file tree
Showing 23 changed files with 845 additions and 88 deletions.
21 changes: 16 additions & 5 deletions .github/workflows/test.yml
@@ -1,7 +1,6 @@
name: Unit tests

on:
pull_request:
push:
paths:
- "**.go"
Expand All @@ -16,13 +15,17 @@ jobs:
runs-on: ubuntu-20.04
permissions:
pull-requests: write
# Required: allow read access to the content for analysis.
contents: read
# Optional: Allow write access to checks to allow the action to annotate code in the PR.
checks: write
steps:
- name: Checkout
uses: actions/checkout@v3
- uses: actions/setup-go@v4
uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.20"
- uses: actions/cache@v3
- uses: actions/cache@v4
with:
path: |
~/go/pkg/mod
Expand All @@ -42,14 +45,22 @@ jobs:
run: |
cd ./example/hasura
docker-compose up -d
- name: Lint
uses: golangci/golangci-lint-action@v3
with:
version: latest
only-new-issues: true
skip-cache: false
- name: Run Go unit tests
run: go test -v -race -timeout 3m -coverprofile=coverage.out ./...
- name: Go coverage format
if: ${{ github.event_name == 'pull_request' }}
run: |
go get github.com/boumenot/gocover-cobertura
go install github.com/boumenot/gocover-cobertura
gocover-cobertura < coverage.out > coverage.xml
- name: Code Coverage Summary Report
if: ${{ github.event_name == 'pull_request' }}
uses: irongut/CodeCoverageSummary@v1.3.0
with:
filename: coverage.xml
Expand All @@ -63,7 +74,7 @@ jobs:
thresholds: "60 80"
- name: Add Coverage PR Comment
uses: marocchino/sticky-pull-request-comment@v2
if: ${{ github.event_name == 'pull_request_target' }}
if: ${{ github.event_name == 'pull_request' }}
with:
path: code-coverage-results.md
- name: Dump docker logs on failure
Expand Down
10 changes: 10 additions & 0 deletions README.md
Expand Up @@ -571,6 +571,16 @@ client := graphql.NewSubscriptionClient("wss://example.com/graphql").
})
```
Some servers validate custom auth tokens on the header instead. To authenticate with headers, use `WebsocketOptions`:
```go
client := graphql.NewSubscriptionClient(serverEndpoint).
WithWebSocketOptions(graphql.WebsocketOptions{
HTTPHeader: http.Header{
"Authorization": []string{"Bearer random-secret"},
},
})
```
#### Options
Expand Down
33 changes: 33 additions & 0 deletions example/graphql-ws-bc/README.md
@@ -0,0 +1,33 @@
# Subscription example with graphql-ws backwards compatibility

The example demonstrates the subscription client with the native graphql-ws Node.js server, using [ws server usage with subscriptions-transport-ws backwards compatibility](https://the-guild.dev/graphql/ws/recipes#ws-server-usage-with-subscriptions-transport-ws-backwards-compatibility) and [custom auth handling](https://the-guild.dev/graphql/ws/recipes#server-usage-with-ws-and-custom-auth-handling) recipes. The client authenticates with the server via HTTP header.

```go
client := graphql.NewSubscriptionClient(serverEndpoint).
WithWebSocketOptions(graphql.WebsocketOptions{
HTTPHeader: http.Header{
"Authorization": []string{"Bearer random-secret"},
},
})
```

## Get started

### Server

Requires Node.js and npm

```bash
cd server
npm install
npm start
```

The server will be hosted on `localhost:4000`.

### Client

```bash
go run ./client
```

85 changes: 85 additions & 0 deletions example/graphql-ws-bc/client/main.go
@@ -0,0 +1,85 @@
// subscription is a test program currently being used for developing graphql package.
// It performs queries against a local test GraphQL server instance.
//
// It's not meant to be a clean or readable example. But it's functional.
// Better, actual examples will be created in the future.
package main

import (
"flag"
"log"
"net/http"

graphql "github.com/hasura/go-graphql-client"
)

func main() {
protocol := graphql.GraphQLWS
protocolArg := flag.String("protocol", "graphql-ws", "The protocol is used for the subscription")
flag.Parse()

if protocolArg != nil {
switch *protocolArg {
case "graphql-ws":
case "":
case "ws":
protocol = graphql.SubscriptionsTransportWS
default:
panic("invalid protocol. Accept [ws, graphql-ws]")
}
}

if err := startSubscription(protocol); err != nil {
panic(err)
}
}

const serverEndpoint = "http://localhost:4000"

func startSubscription(protocol graphql.SubscriptionProtocolType) error {
log.Printf("start subscription with protocol: %s", protocol)
client := graphql.NewSubscriptionClient(serverEndpoint).
WithWebSocketOptions(graphql.WebsocketOptions{
HTTPHeader: http.Header{
"Authorization": []string{"Bearer random-secret"},
},
}).
WithLog(log.Println).
WithProtocol(protocol).
WithoutLogTypes(graphql.GQLData, graphql.GQLConnectionKeepAlive).
OnError(func(sc *graphql.SubscriptionClient, err error) error {
log.Print("err", err)
return err
})

defer client.Close()

/*
subscription {
greetings
}
*/
var sub struct {
Greetings string `graphql:"greetings"`
}

_, err := client.Subscribe(sub, nil, func(data []byte, err error) error {

if err != nil {
log.Println(err)
return nil
}

if data == nil {
return nil
}
log.Printf("hello: %+v", string(data))
return nil
})

if err != nil {
panic(err)
}

return client.Run()
}
1 change: 1 addition & 0 deletions example/graphql-ws-bc/server/.gitignore
@@ -0,0 +1 @@
node_modules
86 changes: 86 additions & 0 deletions example/graphql-ws-bc/server/index.ts
@@ -0,0 +1,86 @@
// The example is copied from ws server usage with subscriptions-transport-ws backwards compatibility example
// https://the-guild.dev/graphql/ws/recipes#ws-server-usage-with-subscriptions-transport-ws-backwards-compatibility

import http from "http";
import { WebSocketServer } from "ws"; // yarn add ws
// import ws from 'ws'; yarn add ws@7
// const WebSocketServer = ws.Server;
import { execute, subscribe } from "graphql";
import { GRAPHQL_TRANSPORT_WS_PROTOCOL } from "graphql-ws";
import { useServer } from "graphql-ws/lib/use/ws";
import { SubscriptionServer, GRAPHQL_WS } from "subscriptions-transport-ws";
import { schema } from "./schema";

// extra in the context
interface Extra {
readonly request: http.IncomingMessage;
}

// your custom auth
class Forbidden extends Error {}
function handleAuth(request: http.IncomingMessage) {
// do your auth on every subscription connect
const token = request.headers["authorization"];

// or const { iDontApprove } = session(request.cookies);
if (token !== "Bearer random-secret") {
// throw a custom error to be handled
throw new Forbidden(":(");
}
}

// graphql-ws
const graphqlWs = new WebSocketServer({ noServer: true });
useServer(
{
schema,
onConnect: async (ctx) => {
// do your auth on every connect (recommended)
await handleAuth(ctx.extra.request);
},
},
graphqlWs
);

// subscriptions-transport-ws
const subTransWs = new WebSocketServer({ noServer: true });
SubscriptionServer.create(
{
schema,
execute,
subscribe,
},
subTransWs
);

// create http server
const server = http.createServer(function weServeSocketsOnly(_, res) {
res.writeHead(404);
res.end();
});

// listen for upgrades and delegate requests according to the WS subprotocol
server.on("upgrade", (req, socket, head) => {
// extract websocket subprotocol from header
const protocol = req.headers["sec-websocket-protocol"];
const protocols = Array.isArray(protocol)
? protocol
: protocol?.split(",").map((p) => p.trim());

// decide which websocket server to use
const wss =
protocols?.includes(GRAPHQL_WS) && // subscriptions-transport-ws subprotocol
!protocols.includes(GRAPHQL_TRANSPORT_WS_PROTOCOL) // graphql-ws subprotocol
? subTransWs
: // graphql-ws will welcome its own subprotocol and
// gracefully reject invalid ones. if the client supports
// both transports, graphql-ws will prevail
graphqlWs;
wss.handleUpgrade(req, socket, head, (ws) => {
wss.emit("connection", ws, req);
});
});

const port = 4000;
console.log(`listen server on localhost:${port}`);
server.listen(port);

0 comments on commit 8f1d49e

Please sign in to comment.