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

Get panic cause as error in Error hooks #160

Merged
merged 7 commits into from
Apr 2, 2019
Merged

Conversation

marioizquierdo
Copy link
Contributor

Currently, service method handlers make sure that panics still call Error hooks. However, the twirp.Error passed to the error hook has no information about the panic, it is simply an Internal error with Msg: "Internal service panic".

This PR adds a Cause to the error passed to the Error hooks, so it can be unwrapped with pkg/errors.Cause. The resulting error response is the same, with Msg: "Internal service panic", the information about the panic is only available in the server through accessing the cause.

// Given hooks with Error handling ...
hooks := &twirp.ServerHooks{
	Error: func(ctx context.Context, twerr twirp.Error) context.Context {		
		// Some errors are wrapped as Internal errors. Panics are also wrapped here.
		err := pkgerrors.Cause(twerr)

		// Before this PR
		twerr.Msg() //=> "Internal service panic"
		err.Error() //=> "twirp error internal: Internal service panic"

		// After this PR
		twerr.Msg() //=> "Internal service panic"
		err.Error() //=> shows the original error, or "panic: %v" if the panic value is not an error
	},
}

In addition, I made a little bit of refactor to simplify and explain the code that handles panics.

func AddImport(fset *token.FileSet, f *ast.File, ipath string) (added bool) {
return AddNamedImport(fset, f, "", ipath)
func AddImport(fset *token.FileSet, f *ast.File, path string) (added bool) {
return AddNamedImport(fset, f, "", path)
Copy link
Contributor Author

@marioizquierdo marioizquierdo Mar 15, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, all of this appeared when I first ran make test_core.
I wonder if I can git reset this, or if we should keep this diff around to keep tools up to date. I tried to mitigate some of this in another PR: #159 but that didn't update the tools. I'm not sure I understand what is going on here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to rethink our build tooling in general, maybe moving to doing everything in Docker. I'll just accept these changes for now.

@marioizquierdo
Copy link
Contributor Author

Fixes #163

Copy link
Contributor

@spenczar spenczar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks basically good - I agree with the goal here.

// The panic is not intercepted and should be handled in middleware to avoid empty responses.
func handlePanics(ctx context.Context, resp http.ResponseWriter, hooks *twirp.ServerHooks) {
if r := recover(); r != nil {
err := errFromPanic(r) // allow to inspect error on Error hooks
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These comments are overflowing way off to the right side. Could we get them to stay under ~100 characters, either by moving them above the lines they describe or by not vertically aligning them?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They seem to auto-align when generating code. I didn't realize that was happening. Fixing ...

@@ -802,7 +803,7 @@ func TestPanickyApplication(t *testing.T) {
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if r := recover(); r == nil {
t.Error("http server never panicked")
t.Error("http server never panicked") // also make sure it did panic
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this comment is useful.

errHookCalled = true
// The error should have a .Cause containing the panic value for inspection
err := pkgerrors.Cause(twerr)
if err.Error() != "This Is Fine Meme" {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than testing the error string, you should be able to test whether err == panicValue directly.

server := httptest.NewServer(h)
defer server.Close()

client := NewHaberdasherJSONClient(server.URL, http.DefaultClient)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be good to test both JSON and Protobuf code paths.

_ = writeErr
}

callResponseSent(ctx, hooks)
callResponseSent(ctx, hooks) // ResponseSent hook
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this comment

// hook, or just silently ignore the error.
//
// Logging is unacceptable because we don't have a user-controlled
// Most likely the connection is broken.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer the old comment that was here, could you revert this change?

@@ -381,32 +365,23 @@ func writeError(ctx context.Context, resp http.ResponseWriter, err error, hooks

statusCode := twirp.ServerHTTPStatusFromErrorCode(twerr.Code())
ctx = ctxsetters.WithStatusCode(ctx, statusCode)
ctx = callError(ctx, hooks, twerr)
ctx = callError(ctx, hooks, twerr) // Error hook
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this comment

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to rename callError to callErrorHook? and callResponseSent to callResponseSentHook? etc. No big deal, I just found "callError" to be a little misleading.

func (e *wrappedError) Cause() error { return e.cause }
func (e *wrappedError) Error() string { return e.prefix + ": " + e.cause.Error() }

// handlePanics makes sure that panics during rpc methods respond with Twirp Interna
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: Internal


// handlePanics makes sure that panics during rpc methods respond with Twirp Interna
// error responses (status 500) and trigger hooks with the panic wrapped as an error.
// The panic is not intercepted and should be handled in middleware to avoid empty responses.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd phrase this differently. The panic is definitely intercepted, we just re-raise it.

Empty responses are kind of a separate issue that we should be able to deal with by flushing correctly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. I'll phrase it differently to be more clear. I found the empty response issue while working on this, it was not as clear to me before.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For reference, the empty responses issue is tracked here #163

Copy link
Contributor

@spenczar spenczar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One minor bug in a test, but LGTM other than that.

if err == nil {
t.Fatalf("%s client: err was expected for panicking handler, found nil", name)
}
if !errHookCalled {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to reset this variable to false after the first loop iteration

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(fixed it myself)

func AddImport(fset *token.FileSet, f *ast.File, ipath string) (added bool) {
return AddNamedImport(fset, f, "", ipath)
func AddImport(fset *token.FileSet, f *ast.File, path string) (added bool) {
return AddNamedImport(fset, f, "", path)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to rethink our build tooling in general, maybe moving to doing everything in Docker. I'll just accept these changes for now.

if err == nil {
t.Fatalf("%s client: err was expected for panicking handler, found nil", name)
}
if !errHookCalled {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(fixed it myself)

@spenczar spenczar merged commit ad6d075 into master Apr 2, 2019
@marioizquierdo marioizquierdo deleted the better_panics_handling branch July 28, 2019 05:53
Sign up for free to 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