Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add more WebSocket options for subscription client #126

Merged
merged 4 commits into from
Feb 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -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/[email protected]
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
86 changes: 86 additions & 0 deletions example/graphql-ws-bc/server/index.ts
Original file line number Diff line number Diff line change
@@ -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);
Loading
Loading