Skip to content

fix: add nil checks for req.Filters to prevent panic on empty request…#109

Open
CN-Antonio wants to merge 1 commit into
Project-HAMi:mainfrom
CN-Antonio:fix/nil-pointer-panic-on-empty-filters
Open

fix: add nil checks for req.Filters to prevent panic on empty request…#109
CN-Antonio wants to merge 1 commit into
Project-HAMi:mainfrom
CN-Antonio:fix/nil-pointer-panic-on-empty-filters

Conversation

@CN-Antonio

@CN-Antonio CN-Antonio commented Jul 4, 2026

Copy link
Copy Markdown

… bodies

When the Go backend receives a request with empty body {}, protobuf deserialization sets req.Filters to nil. The service layer then accesses filters.DeviceId etc. without nil checks, causing nil pointer dereference.

This affects /v1/summary, /v1/nodes, /v1/gpus, /v1/containers and their detail endpoints. The built-in Vue frontend is unaffected because it always includes the filters field.

Changes:

  • service/node.go: nil guards on GetSummary, GetAllNodes, GetNode
  • service/card.go: nil guards on GetAllGPUs, GetAllGPUTypes, GetGPU
  • service/container.go: nil guard on GetAllContainers
  • data/node.go: add Ready() flag set after informer sync
  • data/pod.go: add Ready() flag set after informer sync
  • biz/node.go: add Ready() to NodeRepo interface
  • biz/pod.go: add Ready() to PodRepo interface
  • server/http.go: /readyz returns 503 when informers not synced

Fixes #108

Summary by CodeRabbit

  • New Features
    • Added readiness reporting across the backend (repo/use-case/service) and exposed it via a service-level Ready() method.
    • Updated the /readyz endpoint to return 200 ok only when node data is ready; otherwise it returns 503 not ready.
  • Bug Fixes
    • Prevented nil-pointer crashes by defensively handling nil requests and nil filters in GPU, container, and node endpoints.
    • Improved readiness determination by correctly evaluating informer cache-sync results and tracking readiness state after initialization.

@hami-robot

hami-robot Bot commented Jul 4, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: CN-Antonio

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@hami-robot

hami-robot Bot commented Jul 4, 2026

Copy link
Copy Markdown

Welcome @CN-Antonio! It looks like this is your first PR to Project-HAMi/HAMi-WebUI 🎉

@hami-robot hami-robot Bot added the size/L label Jul 4, 2026
@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: cd43915e-6783-4ad6-a995-87b51b0d060a

📥 Commits

Reviewing files that changed from the base of the PR and between 418514b and 91b10e0.

📒 Files selected for processing (8)
  • server/internal/biz/node.go
  • server/internal/biz/pod.go
  • server/internal/data/node.go
  • server/internal/data/pod.go
  • server/internal/server/http.go
  • server/internal/service/card.go
  • server/internal/service/container.go
  • server/internal/service/node.go
✅ Files skipped from review due to trivial changes (1)
  • server/internal/service/container.go
🚧 Files skipped from review as they are similar to previous changes (7)
  • server/internal/server/http.go
  • server/internal/service/card.go
  • server/internal/biz/node.go
  • server/internal/data/node.go
  • server/internal/data/pod.go
  • server/internal/service/node.go
  • server/internal/biz/pod.go

📝 Walkthrough

Walkthrough

Adds nil-request guards to node, card, and container handlers, and adds readiness state from node/pod repos through usecases into NodeService and /readyz.

Changes

Nil pointer guards for empty request bodies

Layer / File(s) Summary
Node service nil guards
server/internal/service/node.go
GetSummary, GetAllNodes, and GetNode default to empty filters or return empty replies when req or req.Filters is nil.
Card service nil guards
server/internal/service/card.go
GetAllGPUs, GetAllGPUTypes, and GetGPU initialize default filters or return early when req or req.Filters is nil.
Container service nil guard
server/internal/service/container.go
GetAllContainers conditionally reads req.Filters and falls back to an empty filters struct.

Informer readiness reporting

Layer / File(s) Summary
Repo and usecase Ready() contracts
server/internal/biz/node.go, server/internal/biz/pod.go
NodeRepo and PodRepo interfaces gain Ready() bool; NodeUsecase and PodUseCase delegate to repo.Ready().
Node data readiness tracking
server/internal/data/node.go
nodeRepo adds a ready field set during updateLocalNodes, improved cache-sync logging, and a Ready() accessor.
Pod data readiness tracking
server/internal/data/pod.go
podRepo adds a ready field set only after all informer caches sync successfully, with a Ready() accessor.
NodeService and /readyz wiring
server/internal/service/node.go, server/internal/server/http.go
NodeService.Ready() combines node/pod readiness; /readyz returns 200/"ok" or 503/"not ready" accordingly.

Estimated code review effort: 2 (Simple) | ~15 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant HTTPHandler as "/readyz Handler"
  participant NodeService
  participant NodeUsecase
  participant PodUseCase
  participant NodeRepo
  participant PodRepo

  Client->>HTTPHandler: GET /readyz
  HTTPHandler->>NodeService: Ready()
  NodeService->>NodeUsecase: Ready()
  NodeUsecase->>NodeRepo: Ready()
  NodeRepo-->>NodeUsecase: bool
  NodeService->>PodUseCase: Ready()
  PodUseCase->>PodRepo: Ready()
  PodRepo-->>PodUseCase: bool
  NodeService-->>HTTPHandler: combined bool
  alt ready
    HTTPHandler-->>Client: 200 "ok"
  else not ready
    HTTPHandler-->>Client: 503 "not ready"
  end
Loading

Poem

A nil request once made me panic and flee,
Now empty filters just default sweetly~
Informers sync, I hop and I peek,
“Ready?” I ask before I speak. 🐇
/readyz now tells the truth, no lies,
503 or “ok” — no more surprise!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main bug fix: adding nil checks for req.Filters to prevent empty-request panics.
Linked Issues check ✅ Passed The PR adds nil guards for the affected endpoints and updates readiness reporting as required by issue #108.
Out of Scope Changes check ✅ Passed The readiness updates are part of the linked issue scope, and no unrelated changes are evident.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

… bodies

When the Go backend receives a request with empty body {}, protobuf
deserialization sets req.Filters to nil. The service layer then accesses
filters.DeviceId etc. without nil checks, causing nil pointer dereference.

This affects /v1/summary, /v1/nodes, /v1/gpus, /v1/containers and their
detail endpoints. The built-in Vue frontend is unaffected because it always
includes the filters field.

Changes:
- service/node.go: nil guards on GetSummary, GetAllNodes, GetNode
- service/card.go: nil guards on GetAllGPUs, GetAllGPUTypes, GetGPU
- service/container.go: nil guard on GetAllContainers
- data/node.go: add Ready() flag set after informer sync
- data/pod.go: add Ready() flag set after informer sync
- biz/node.go: add Ready() to NodeRepo interface
- biz/pod.go: add Ready() to PodRepo interface
- server/http.go: /readyz returns 503 when informers not synced

Fixes Project-HAMi#108

Signed-off-by: CN-Antonio <24762952+CN-Antonio@users.noreply.github.com>
@CN-Antonio CN-Antonio force-pushed the fix/nil-pointer-panic-on-empty-filters branch from 418514b to 91b10e0 Compare July 4, 2026 17:46

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
server/internal/service/card.go (1)

24-30: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Same manual nil-guard duplicated twice in this file — simplify with GetFilters().

Both GetAllGPUs and GetAllGPUTypes repeat the identical if req != nil { filters = req.Filters } pattern. The generated getter req.GetFilters() already nil-checks the receiver, so this can be simplified and de-duplicated.

♻️ Proposed simplification
 func (s *CardService) GetAllGPUs(ctx context.Context, req *pb.GetAllGpusReq) (*pb.GPUsReply, error) {
-	var filters *pb.GetAllGpusReq_Filters
-	if req != nil {
-		filters = req.Filters
-	}
+	filters := req.GetFilters()
 	if filters == nil {
 		filters = &pb.GetAllGpusReq_Filters{}
 	}
 func (s *CardService) GetAllGPUTypes(ctx context.Context, req *pb.GetAllGpusReq) (*pb.GPUsReply, error) {
 	...
-	var filters *pb.GetAllGpusReq_Filters
-	if req != nil {
-		filters = req.Filters
-	}
+	filters := req.GetFilters()
 	if filters == nil {
 		filters = &pb.GetAllGpusReq_Filters{}
 	}

Since both methods take the same request type, consider extracting a small local helper (e.g. defaultGpusFilters(req *pb.GetAllGpusReq) *pb.GetAllGpusReq_Filters) to avoid the duplicated fallback logic entirely.

Also applies to: 124-130

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/internal/service/card.go` around lines 24 - 30, The request filter
fallback logic in GetAllGPUs and GetAllGPUTypes is duplicated and manually
nil-checks the request before reading Filters. Replace the repeated `if req !=
nil { filters = req.Filters }` pattern with the generated `GetFilters()`
accessor on the request type, and factor the defaulting logic into a small local
helper such as `defaultGpusFilters` so both methods share the same fallback
behavior without duplication.
server/internal/service/container.go (1)

48-54: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Simplify nil-guard using generated GetFilters() getter.

Same pattern as node.go/card.go: req.GetFilters() already nil-checks the receiver, making the manual if req != nil block unnecessary.

♻️ Proposed simplification
 func (s *ContainerService) GetAllContainers(ctx context.Context, req *pb.GetAllContainersReq) (*pb.ContainersReply, error) {
-	var filters *pb.GetAllContainersReq_Filters
-	if req != nil {
-		filters = req.Filters
-	}
+	filters := req.GetFilters()
 	if filters == nil {
 		filters = &pb.GetAllContainersReq_Filters{}
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/internal/service/container.go` around lines 48 - 54, Simplify the
nil-guard in GetAllContainers by using the generated req.GetFilters() getter
instead of manually checking req and assigning req.Filters. Update the container
service code path to mirror the existing node.go/card.go pattern, keeping the
fallback to an empty pb.GetAllContainersReq_Filters when the getter returns nil.
server/internal/service/node.go (1)

34-55: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Simplify nil-guard using generated GetFilters() getter.

Standard protoc-gen-go messages generate nil-safe getters (func (x *T) GetFilters() *T_Filters { if x != nil { return x.Filters }; return nil }), so the manual if req != nil { filters = req.Filters } block is redundant — req.GetFilters() already handles a nil req.

♻️ Proposed simplification
 func (s *NodeService) GetSummary(ctx context.Context, req *pb.GetSummaryReq) (*pb.DeviceSummaryReply, error) {
-	var filters *pb.GetSummaryReq_Filters
-	if req != nil {
-		filters = req.Filters
-	}
+	filters := req.GetFilters()
 	var res = &pb.DeviceSummaryReply{}
 	if filters == nil {
 		filters = &pb.GetSummaryReq_Filters{}
 	}
 func (s *NodeService) GetAllNodes(ctx context.Context, req *pb.GetAllNodesReq) (*pb.NodesReply, error) {
-	var filters *pb.GetAllNodesReq_Filters
-	if req != nil {
-		filters = req.Filters
-	}
+	filters := req.GetFilters()
 	if filters == nil {
 		filters = &pb.GetAllNodesReq_Filters{}
 	}

Please confirm the generated getters exist for GetSummaryReq/GetAllNodesReq before applying (standard for protoc-gen-go output, but worth a quick check).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/internal/service/node.go` around lines 34 - 55, Simplify the nil
handling in NodeService.GetSummary and NodeService.GetAllNodes by using the
generated nil-safe GetFilters() accessor on GetSummaryReq and GetAllNodesReq
instead of manually checking req for nil and reading req.Filters. Verify the
protoc-gen-go generated getters exist for these request types, then replace the
temporary filters setup with the getter-based flow so the methods remain
nil-safe and shorter.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/internal/data/node.go`:
- Line 33: The readiness state in nodeRepo is being set from local cache
population even when informer sync fails, so /readyz can become true too early.
Update the nodeRepo readiness logic around init(), Ready(), and updateLocalNodes
so readiness is only enabled when the initial sync succeeds, not just when data
is populated. Use the existing syncedOK handling in nodeRepo to gate Ready() and
keep ready false if cache sync fails.

---

Nitpick comments:
In `@server/internal/service/card.go`:
- Around line 24-30: The request filter fallback logic in GetAllGPUs and
GetAllGPUTypes is duplicated and manually nil-checks the request before reading
Filters. Replace the repeated `if req != nil { filters = req.Filters }` pattern
with the generated `GetFilters()` accessor on the request type, and factor the
defaulting logic into a small local helper such as `defaultGpusFilters` so both
methods share the same fallback behavior without duplication.

In `@server/internal/service/container.go`:
- Around line 48-54: Simplify the nil-guard in GetAllContainers by using the
generated req.GetFilters() getter instead of manually checking req and assigning
req.Filters. Update the container service code path to mirror the existing
node.go/card.go pattern, keeping the fallback to an empty
pb.GetAllContainersReq_Filters when the getter returns nil.

In `@server/internal/service/node.go`:
- Around line 34-55: Simplify the nil handling in NodeService.GetSummary and
NodeService.GetAllNodes by using the generated nil-safe GetFilters() accessor on
GetSummaryReq and GetAllNodesReq instead of manually checking req for nil and
reading req.Filters. Verify the protoc-gen-go generated getters exist for these
request types, then replace the temporary filters setup with the getter-based
flow so the methods remain nil-safe and shorter.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1fb1c1a2-c8e0-4b66-b329-0d06fbac801f

📥 Commits

Reviewing files that changed from the base of the PR and between c59f776 and 418514b.

📒 Files selected for processing (8)
  • server/internal/biz/node.go
  • server/internal/biz/pod.go
  • server/internal/data/node.go
  • server/internal/data/pod.go
  • server/internal/server/http.go
  • server/internal/service/card.go
  • server/internal/service/container.go
  • server/internal/service/node.go

log *log.Helper
mutex sync.RWMutex
providers []provider.Provider
ready bool

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

nodeRepo.Ready() should stay false when cache sync fails. syncedOK is only used for logging in init(), while Ready() still flips on when updateLocalNodes populates data. That can expose /readyz as ready even after a failed informer sync; gate readiness on sync success as well.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/internal/data/node.go` at line 33, The readiness state in nodeRepo is
being set from local cache population even when informer sync fails, so /readyz
can become true too early. Update the nodeRepo readiness logic around init(),
Ready(), and updateLocalNodes so readiness is only enabled when the initial sync
succeeds, not just when data is populated. Use the existing syncedOK handling in
nodeRepo to gate Ready() and keep ready false if cache sync fails.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] nil pointer panic on API endpoints when request body is {}

1 participant