Skip to content

Experimental: Add grpc-web HTTP client #2174

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

Closed
Closed
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
16 changes: 3 additions & 13 deletions clientconn.go
Original file line number Diff line number Diff line change
Expand Up @@ -478,19 +478,9 @@ func DialContext(ctx context.Context, target string, opts ...DialOption) (conn *
}
}

if !cc.dopts.insecure {
if cc.dopts.copts.TransportCredentials == nil {
return nil, errNoTransportSecurity
}
} else {
if cc.dopts.copts.TransportCredentials != nil {
return nil, errCredentialsConflict
}
for _, cd := range cc.dopts.copts.PerRPCCredentials {
if cd.RequireTransportSecurity() {
return nil, errTransportCredentialsMissing
}
}
err = cc.validateClientOptions()
if err != nil {
return nil, err
}

cc.mkp = cc.dopts.copts.KeepaliveParams
Expand Down
39 changes: 39 additions & 0 deletions dial.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// +build !js !wasm

/*
*
* Copyright 2018 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package grpc

func (cc *ClientConn) validateClientOptions() error {
if !cc.dopts.insecure {
if cc.dopts.copts.TransportCredentials == nil {
return errNoTransportSecurity
}
} else {
if cc.dopts.copts.TransportCredentials != nil {
return errCredentialsConflict
}
for _, cd := range cc.dopts.copts.PerRPCCredentials {
if cd.RequireTransportSecurity() {
return errTransportCredentialsMissing
}
}
}
return nil
}
39 changes: 39 additions & 0 deletions dial_js.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// +build js,wasm

/*
*
* Copyright 2018 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package grpc

import (
"errors"
)

var (
// errTransportSecurity indicates the client was attempting an RPC call
// from a WASM client with TransportCredentials configured.
errTransportSecurity = errors.New("cannot configure transport security for WASM clients")
)

func (cc *ClientConn) validateClientOptions() error {
if cc.dopts.copts.TransportCredentials != nil {
return errTransportSecurity
}

return nil
}
27 changes: 27 additions & 0 deletions newstream.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// +build !js !wasm

/*
*
* Copyright 2018 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package grpc

import "golang.org/x/net/context"

func (cc *ClientConn) newStream(ctx context.Context, desc *StreamDesc, method string, opts ...CallOption) (ClientStream, error) {
return cc.makeStream(ctx, desc, method, opts...)
}
34 changes: 34 additions & 0 deletions newstream_js.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// +build js,wasm

/*
*
* Copyright 2018 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package grpc

import (
"golang.org/x/net/context"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

func (cc *ClientConn) newStream(ctx context.Context, desc *StreamDesc, method string, opts ...CallOption) (ClientStream, error) {
if desc.ClientStreams {
return nil, status.Error(codes.Unimplemented, "client-side and bi-directional streaming is not yet supported by gRPC-web")
}
return cc.makeStream(ctx, desc, method, opts...)
}
27 changes: 27 additions & 0 deletions parse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// +build !js !wasm

/*
*
* Copyright 2018 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package grpc

import "google.golang.org/grpc/transport"

func parseMsg(p *parser, s *transport.Stream, maxReceiveMessageSize int, pf payloadFormat, d []byte) ([]byte, error) {
return d, nil
}
79 changes: 79 additions & 0 deletions parse_js.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// +build js,wasm

/*
*
* Copyright 2018 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package grpc

import (
"bufio"
"bytes"
"errors"
"io"
"net/http"
"strings"

"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/grpc/transport"
)

func parseMsg(p *parser, s *transport.Stream, maxReceiveMessageSize int, pf payloadFormat, d []byte) ([]byte, error) {
if pf == trailer || pf == compressedTrailer {
trailers, err := readTrailers(bytes.NewReader(d), len(d))
if err != nil {
return nil, status.Errorf(codes.Internal, "grpc: failed to parse returned trailers: %v", err)
}
// Holy layer violation :(
// This is necessary because the gRPC-Web trailers are part of the response body. See
// https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md#protocol-differences-vs-grpc-over-http2
err = s.SetTrailers(trailers)
if err != nil {
return nil, status.Errorf(codes.Internal, "grpc: failed to set trailers on stream: %v", err)
}
// Sanity check that parser is done. This should return io.EOF
_, _, err = p.recvMsg(maxReceiveMessageSize)
if err == io.EOF {
return nil, err
}
return nil, status.Errorf(codes.Internal, "grpc: unexpected error reading last message from stream: %v", err)
}
return d, nil
}

// readTrailers consumes the rest of the reader and parses the
// contents as "\r\n"-separated trailers, in accordance with
// https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md#protocol-differences-vs-grpc-over-http2
func readTrailers(in io.Reader, bufSize int) (http.Header, error) {
s := bufio.NewScanner(in)
buf := make([]byte, bufSize)
s.Buffer(buf, len(buf))
// Uses http.Header instead of metadata.MD as .Add method
// normalizes trailer keys.
trailers := http.Header{}
for s.Scan() {
v := s.Text()
kv := strings.SplitN(v, ": ", 2)
if len(kv) != 2 {
return nil, errors.New("malformed trailer: " + v)
}
trailers.Add(kv[0], kv[1])
}

return trailers, s.Err()
}
14 changes: 11 additions & 3 deletions rpc_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,10 @@ type payloadFormat uint8
const (
compressionNone payloadFormat = 0 // no compression
compressionMade payloadFormat = 1 // compressed
// Defined for gRPC-Web
// https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md#protocol-differences-vs-grpc-over-http2
trailer payloadFormat = 1 << 7
compressedTrailer payloadFormat = (1 << 7) | 1
)

// parser reads complete gRPC messages from the underlying reader.
Expand Down Expand Up @@ -557,8 +561,8 @@ func outPayload(client bool, msg interface{}, data, payload []byte, t time.Time)

func checkRecvPayload(pf payloadFormat, recvCompress string, haveCompressor bool) *status.Status {
switch pf {
case compressionNone:
case compressionMade:
case compressionNone, trailer:
case compressionMade, compressedTrailer:
if recvCompress == "" || recvCompress == encoding.Identity {
return status.New(codes.Internal, "grpc: compressed flag set with identity or empty encoding")
}
Expand Down Expand Up @@ -587,7 +591,7 @@ func recv(p *parser, c baseCodec, s *transport.Stream, dc Decompressor, m interf
return st.Err()
}

if pf == compressionMade {
if pf == compressionMade || pf == compressedTrailer {
// To match legacy behavior, if the decompressor is set by WithDecompressor or RPCDecompressor,
// use this decompressor as the default.
if dc != nil {
Expand All @@ -611,6 +615,10 @@ func recv(p *parser, c baseCodec, s *transport.Stream, dc Decompressor, m interf
// implementation.
return status.Errorf(codes.ResourceExhausted, "grpc: received message larger than max (%d vs. %d)", len(d), maxReceiveMessageSize)
}
d, err = parseMsg(p, s, maxReceiveMessageSize, pf, d)
if err != nil {
return err // already a status error (or io.EOF)
}
if err := c.Unmarshal(d, m); err != nil {
return status.Errorf(codes.Internal, "grpc: failed to unmarshal the received message %v", err)
}
Expand Down
4 changes: 4 additions & 0 deletions stream.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ type ClientStream interface {
// If none of the above happen, a goroutine and a context will be leaked, and grpc
// will not call the optionally-configured stats handler with a stats.End message.
func (cc *ClientConn) NewStream(ctx context.Context, desc *StreamDesc, method string, opts ...CallOption) (ClientStream, error) {
return cc.newStream(ctx, desc, method, opts...)
}

func (cc *ClientConn) makeStream(ctx context.Context, desc *StreamDesc, method string, opts ...CallOption) (ClientStream, error) {
// allow interceptor to see all applicable call options, which means those
// configured as defaults from dial option as well as per-call options
opts = combine(cc.dopts.callOptions, opts)
Expand Down
Loading