func (h handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { start := time.Now() defer req.Body.Close() if req.Method != "POST" { http.NotFound(w, req) return } service := req.Header.Get(ServiceHeader) procedure := req.Header.Get(ProcedureHeader) err := h.callHandler(w, req, start) if err == nil { return } err = errors.AsHandlerError(service, procedure, err) status := http.StatusInternalServerError if transport.IsBadRequestError(err) { status = http.StatusBadRequest } else if transport.IsTimeoutError(err) { status = http.StatusGatewayTimeout } http.Error(w, err.Error(), status) }
// remoteTimeout tests if a yarpc client returns a remote timeout error behind // the TimeoutError interface when a remote tchannel handler returns a handler // timeout. func remoteTimeout(t crossdock.T, dispatcher yarpc.Dispatcher) { assert := crossdock.Assert(t) headers := yarpc.NewHeaders() token := random.Bytes(5) _, _, err := rawCall(dispatcher, headers, "handlertimeout/raw", token) if skipOnConnRefused(t, err) { return } if !assert.Error(err, "expected an error") { return } if transport.IsBadRequestError(err) { t.Skipf("handlertimeout/raw procedure not implemented: %v", err) return } assert.True(transport.IsTimeoutError(err), "returns a TimeoutError: %T", err) form := strings.HasPrefix(err.Error(), `Timeout: call to procedure "handlertimeout/raw" of service "service" from caller "caller" timed out after`) assert.True(form, "must be a remote handler timeout: %q", err.Error()) }
func isUnrecognizedProcedure(err error) bool { if transport.IsBadRequestError(err) { // TODO: Once all other languages implement the gauntlet test // subject, we can remove this check. return strings.Contains(err.Error(), "unrecognized procedure") } return false }
func (h handler) handle(ctx context.Context, call inboundCall) { start := time.Now() err := h.callHandler(ctx, call, start) if err == nil { return } if _, ok := err.(tchannel.SystemError); ok { call.Response().SendSystemError(err) return } err = errors.AsHandlerError(call.ServiceName(), call.MethodString(), err) status := tchannel.ErrCodeUnexpected if transport.IsBadRequestError(err) { status = tchannel.ErrCodeBadRequest } else if transport.IsTimeoutError(err) { status = tchannel.ErrCodeTimeout } call.Response().SendSystemError(tchannel.NewSystemError(status, err.Error())) }
// runRaw tests if a yarpc client returns a remote timeout error behind the // TimeoutError interface when a remote http handler returns a handler timeout. func runRaw(t crossdock.T, disp yarpc.Dispatcher) { assert := crossdock.Assert(t) fatals := crossdock.Fatals(t) ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancel() ch := raw.New(disp.Channel("yarpc-test")) _, _, err := ch.Call(ctx, yarpc.NewReqMeta().Procedure("handlertimeout/raw"), nil) fatals.Error(err, "expected an error") if transport.IsBadRequestError(err) { t.Skipf("handlertimeout/raw method not implemented: %v", err) return } assert.True(transport.IsTimeoutError(err), "returns a TimeoutError: %T", err) form := strings.HasPrefix(err.Error(), `Timeout: call to procedure "handlertimeout/raw" of service "service" from caller "caller" timed out after`) assert.True(form, "must be a remote handler timeout: %q", err.Error()) }
// Run tests if a yarpc client returns correctly a client timeout error behind // the TimeoutError interface when the context deadline is reached while the // server is taking too long to respond. func Run(t crossdock.T) { assert := crossdock.Assert(t) fatals := crossdock.Fatals(t) ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancel() dispatcher := disp.Create(t) fatals.NoError(dispatcher.Start(), "could not start Dispatcher") defer dispatcher.Stop() ch := raw.New(dispatcher.Channel("yarpc-test")) _, _, err := ch.Call(ctx, yarpc.NewReqMeta().Procedure("sleep/raw"), nil) fatals.Error(err, "expected a failure for timeout") if transport.IsBadRequestError(err) { t.Skipf("sleep/raw method not implemented: %v", err) return } assert.True(transport.IsTimeoutError(err), "returns a TimeoutError: %T", err) trans := t.Param(params.Transport) switch trans { case "http": form := strings.HasPrefix(err.Error(), `client timeout for procedure "sleep/raw" of service "yarpc-test" after`) assert.True(form, "should be a client timeout: %q", err.Error()) case "tchannel": form := strings.HasPrefix(err.Error(), `timeout`) assert.True(form, "should be a remote timeout (we cant represent client timeout with tchannel): %q", err.Error()) default: fatals.Fail("", "unknown transport %q", trans) } }
func TestSimpleRoundTrip(t *testing.T) { transports := []roundTripTransport{ httpTransport{t}, tchannelTransport{t}, } tests := []struct { requestHeaders transport.Headers requestBody string responseHeaders transport.Headers responseBody string responseError error wantError func(error) }{ { requestHeaders: transport.NewHeaders().With("token", "1234"), requestBody: "world", responseHeaders: transport.NewHeaders().With("status", "ok"), responseBody: "hello, world", }, { requestBody: "foo", responseError: errors.HandlerUnexpectedError(fmt.Errorf("great sadness")), wantError: func(err error) { assert.True(t, transport.IsUnexpectedError(err), err) assert.Equal(t, "UnexpectedError: great sadness", err.Error()) }, }, { requestBody: "bar", responseError: errors.HandlerBadRequestError(fmt.Errorf("missing service name")), wantError: func(err error) { assert.True(t, transport.IsBadRequestError(err)) assert.Equal(t, "BadRequest: missing service name", err.Error()) }, }, { requestBody: "baz", responseError: errors.RemoteUnexpectedError( `UnexpectedError: error for procedure "foo" of service "bar": great sadness`, ), wantError: func(err error) { assert.True(t, transport.IsUnexpectedError(err)) assert.Equal(t, `UnexpectedError: error for procedure "hello" of service "testService": `+ `UnexpectedError: error for procedure "foo" of service "bar": great sadness`, err.Error()) }, }, { requestBody: "qux", responseError: errors.RemoteBadRequestError( `BadRequest: unrecognized procedure "echo" for service "derp"`, ), wantError: func(err error) { assert.True(t, transport.IsUnexpectedError(err)) assert.Equal(t, `UnexpectedError: error for procedure "hello" of service "testService": `+ `BadRequest: unrecognized procedure "echo" for service "derp"`, err.Error()) }, }, } rootCtx := context.Background() for _, tt := range tests { for _, trans := range transports { requestMatcher := transporttest.NewRequestMatcher(t, &transport.Request{ Caller: testCaller, Service: testService, Procedure: testProcedure, Encoding: raw.Encoding, Headers: tt.requestHeaders, Body: bytes.NewReader([]byte(tt.requestBody)), }) handler := unaryHandlerFunc(func(_ context.Context, r *transport.Request, w transport.ResponseWriter) error { assert.True(t, requestMatcher.Matches(r), "request mismatch: received %v", r) if tt.responseError != nil { return tt.responseError } if tt.responseHeaders.Len() > 0 { w.AddHeaders(tt.responseHeaders) } _, err := w.Write([]byte(tt.responseBody)) assert.NoError(t, err, "failed to write response for %v", r) return err }) ctx, cancel := context.WithTimeout(rootCtx, 200*time.Millisecond) defer cancel() registry := staticRegistry{Handler: handler} trans.WithRegistry(registry, func(o transport.UnaryOutbound) { res, err := o.Call(ctx, &transport.Request{ Caller: testCaller, Service: testService, Procedure: testProcedure, Encoding: raw.Encoding, Headers: tt.requestHeaders, Body: bytes.NewReader([]byte(tt.requestBody)), }) if tt.wantError != nil { if assert.Error(t, err, "%T: expected error, got %v", trans, res) { tt.wantError(err) // none of the errors returned by Call can be valid // Handler errors. _, ok := err.(errors.HandlerError) assert.False(t, ok, "%T: %T must not be a HandlerError", trans, err) } } else { responseMatcher := transporttest.NewResponseMatcher(t, &transport.Response{ Headers: tt.responseHeaders, Body: ioutil.NopCloser(bytes.NewReader([]byte(tt.responseBody))), }) if assert.NoError(t, err, "%T: call failed", trans) { assert.True(t, responseMatcher.Matches(res), "%T: response mismatch", trans) } } }) } } }