From 826c65d0eccf4ebe4790ec3bfc856fff34df7f84 Mon Sep 17 00:00:00 2001 From: Dr Alex Mitre Date: Sun, 4 Oct 2026 04:28:32 -0600 Subject: [PATCH] fix(actions): return 401 for unregistered runner (#39578) ## Summary When an Actions runner's registration has been deleted (or the UUID/token is invalid), `FetchTask` and other authenticated runner RPCs currently return **HTTP 500** ## Change - Return `connect.NewError(connect.CodeUnauthenticated, ...)` via a small `unregisteredRunnerError()` helper for both unregistered / bad-token paths in the interceptor. - Leave Internal `status.Error` paths unchanged (those should remain 5xx). - Add a unit test asserting `connect.CodeOf(err) == connect.CodeUnauthenticated`. Fixes #39576 --------- Signed-off-by: Alex Mitre Co-authored-by: Alex Mitre Co-authored-by: wxiaoguang --- routers/api/actions/runner/interceptor.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/routers/api/actions/runner/interceptor.go b/routers/api/actions/runner/interceptor.go index 8b79962c11d..e981deff303 100644 --- a/routers/api/actions/runner/interceptor.go +++ b/routers/api/actions/runner/interceptor.go @@ -27,6 +27,9 @@ const ( ) var withRunner = connect.WithInterceptors(connect.UnaryInterceptorFunc(func(unaryFunc connect.UnaryFunc) connect.UnaryFunc { + // A plain gRPC status.Error is treated as unknown by Connect and becomes HTTP 500 + // To respond an HTTP status code, use connect.Error instead + errUnregisteredRunner := connect.NewError(connect.CodeUnauthenticated, errors.New("unregistered runner")) return func(ctx context.Context, request connect.AnyRequest) (connect.AnyResponse, error) { methodName := getMethodName(request) if methodName == "Register" { @@ -38,12 +41,12 @@ var withRunner = connect.WithInterceptors(connect.UnaryInterceptorFunc(func(unar runner, err := actions_model.GetRunnerByUUID(ctx, uuid) if err != nil { if errors.Is(err, util.ErrNotExist) { - return nil, status.Error(codes.Unauthenticated, "unregistered runner") + return nil, errUnregisteredRunner } return nil, status.Error(codes.Internal, err.Error()) } if !util.CryptoConstTimeEqual(runner.TokenHash, auth_model.HashToken(token, runner.TokenSalt)) { - return nil, status.Error(codes.Unauthenticated, "unregistered runner") + return nil, errUnregisteredRunner } now := time.Now()