fix(server): resolve golangci-lint contextcheck, errcheck, gocritic findings

contextcheck: add ctx context.Context as first param to ListVisibleChannels,
BlockUser, CreateDM, CreateInvite, UpdateProfile, SendMessage; pass r.Context()
from HTTP handlers and ctx from WS handler; replace context.Background() in
telemetry spans with the propagated ctx.

errcheck: suppress justified Close() errors — defer func(){ _ = rows.Close() }()
in sqlite_events.go (idiomatic; rows.Err() checked), _ = resp.Body.Close() in
host_http.go (body fully consumed), _ = f.Close() in host_ui.go (read-only fd).

gocritic/rangeValCopy: rewrite for _, ch := range all (line 60) to indexed loop
in ChannelService.ListVisibleChannels to avoid 144-byte per-iteration copy.
This commit is contained in:
J3vb
2026-04-07 09:10:14 +02:00
parent 16fbfcce87
commit 81c121c2bd
15 changed files with 43 additions and 42 deletions
+1 -1
View File
@@ -76,7 +76,7 @@ func handleListChannels(svc *service.Services) http.HandlerFunc {
return
}
channels, err := svc.Channels.ListVisibleChannels(user.ID)
channels, err := svc.Channels.ListVisibleChannels(r.Context(), user.ID)
if err != nil {
slog.Error("handleListChannels", "err", err)
writeJSON(w, http.StatusInternalServerError, errorResponse{
+2 -2
View File
@@ -73,7 +73,7 @@ func handleCreateDM(svc *service.Services) http.HandlerFunc {
return
}
result, err := svc.DMs.CreateDM(user.ID, req.RecipientID)
result, err := svc.DMs.CreateDM(r.Context(), user.ID, req.RecipientID)
if err != nil {
writeServiceError(w, err)
return
@@ -169,7 +169,7 @@ func handleBlockUser(svc *service.Services) http.HandlerFunc {
return
}
if err := svc.Blocks.BlockUser(user.ID, targetID); err != nil {
if err := svc.Blocks.BlockUser(r.Context(), user.ID, targetID); err != nil {
writeServiceError(w, err)
return
}
+1 -1
View File
@@ -73,7 +73,7 @@ func handleCreateInvite(svc *service.Services) http.HandlerFunc {
return
}
inv, err := svc.Invites.CreateInvite(user.ID, req.MaxUses, req.ExpiresInHours)
inv, err := svc.Invites.CreateInvite(r.Context(), user.ID, req.MaxUses, req.ExpiresInHours)
if err != nil {
writeServiceError(w, err)
return
+1 -1
View File
@@ -133,7 +133,7 @@ func handleUpdateProfile(svc *service.Services, broadcaster ProfileBroadcaster)
req.Avatar = &trimmed
}
updated, err := svc.Users.UpdateProfile(user.ID, req.Username, req.Avatar)
updated, err := svc.Users.UpdateProfile(r.Context(), user.ID, req.Username, req.Avatar)
if err != nil {
writeServiceError(w, err)
return
+1 -1
View File
@@ -120,7 +120,7 @@ func (r *Registry) HTTPDo(ctx context.Context, inst *Instance, req HTTPRequest)
if err != nil {
return nil, fmt.Errorf("plugin http: do: %w", err)
}
defer resp.Body.Close()
defer func() { _ = resp.Body.Close() }()
// Cap body size so a hostile/large response cannot OOM the host. We
// LimitReader to maxResponseBytes+1 so we can detect truncation.
limited := io.LimitReader(resp.Body, maxResponseBytes+1)
+1 -1
View File
@@ -98,7 +98,7 @@ func (r *Registry) AssetHandler(inst *Instance) http.Handler {
http.NotFound(w, req)
return
}
defer f.Close()
defer func() { _ = f.Close() }()
http.ServeContent(w, req, rel, info.ModTime(), f)
})
}
+2 -2
View File
@@ -22,8 +22,8 @@ func NewBlockService(st store.Store) *BlockService {
// BlockUser blocks a target user. Validates the target exists and
// prevents self-blocking.
func (s *BlockService) BlockUser(blockerID, targetID int64) error {
ctx, span := telemetry.GlobalTracer("service/block").Start(context.Background(), "BlockService.BlockUser",
func (s *BlockService) BlockUser(ctx context.Context, blockerID, targetID int64) error {
ctx, span := telemetry.GlobalTracer("service/block").Start(ctx, "BlockService.BlockUser",
telemetry.Int64("blocker_id", blockerID),
telemetry.Int64("target_id", targetID),
)
+5 -5
View File
@@ -29,9 +29,9 @@ func NewChannelService(st store.Store, perms *PermissionService) *ChannelService
// ListVisibleChannels returns channels the user has ReadMessages permission for.
// DM channels are excluded (they are accessed via DMService).
func (s *ChannelService) ListVisibleChannels(userID int64) ([]db.Channel, error) {
func (s *ChannelService) ListVisibleChannels(ctx context.Context, userID int64) ([]db.Channel, error) {
// Phase B Step 8 — span the public service entrypoint.
ctx, span := telemetry.GlobalTracer("service/channel").Start(context.Background(),
ctx, span := telemetry.GlobalTracer("service/channel").Start(ctx,
"ChannelService.ListVisibleChannels",
telemetry.Int64("user_id", userID),
)
@@ -57,9 +57,9 @@ func (s *ChannelService) ListVisibleChannels(userID int64) ([]db.Channel, error)
if isAdmin {
// Admin sees all non-DM channels.
var visible []db.Channel
for _, ch := range all {
if ch.Type != "dm" {
visible = append(visible, ch)
for i := range all {
if all[i].Type != "dm" {
visible = append(visible, all[i])
}
}
return visible, nil
+2 -2
View File
@@ -30,8 +30,8 @@ type CreateDMResult struct {
// CreateDM creates or retrieves a DM channel between two users.
// Validates that neither user has blocked the other.
func (s *DMService) CreateDM(userID, recipientID int64) (*CreateDMResult, error) {
ctx, span := telemetry.GlobalTracer("service/dm").Start(context.Background(), "DMService.CreateDM",
func (s *DMService) CreateDM(ctx context.Context, userID, recipientID int64) (*CreateDMResult, error) {
ctx, span := telemetry.GlobalTracer("service/dm").Start(ctx, "DMService.CreateDM",
telemetry.Int64("user_id", userID),
telemetry.Int64("recipient_id", recipientID),
)
+2 -2
View File
@@ -27,8 +27,8 @@ const maxInviteExpiryHoursVal = 720
func MaxInviteExpiryHours() int { return maxInviteExpiryHoursVal }
// CreateInvite creates a new invite code with optional max uses and expiry.
func (s *InviteService) CreateInvite(createdBy int64, maxUses int, expiresInHours int) (*db.Invite, error) {
ctx, span := telemetry.GlobalTracer("service/invite").Start(context.Background(), "InviteService.CreateInvite",
func (s *InviteService) CreateInvite(ctx context.Context, createdBy int64, maxUses int, expiresInHours int) (*db.Invite, error) {
ctx, span := telemetry.GlobalTracer("service/invite").Start(ctx, "InviteService.CreateInvite",
telemetry.Int64("created_by", createdBy),
)
start := time.Now()
+2 -2
View File
@@ -116,10 +116,10 @@ func NewMessageService(st store.Store, perms *PermissionService, limiter *auth.R
// SendMessage validates, persists, and prepares broadcast data for a new message.
// Callers are responsible for emitting the appropriate events.
func (s *MessageService) SendMessage(p SendMessageParams) (*SendMessageResult, error) {
func (s *MessageService) SendMessage(ctx context.Context, p SendMessageParams) (*SendMessageResult, error) {
// Phase B Step 8 — wrap the public service entrypoint in a tracing span
// and a duration histogram. Both are no-ops in the default build.
ctx, span := telemetry.GlobalTracer("service/message").Start(context.Background(), "MessageService.SendMessage",
ctx, span := telemetry.GlobalTracer("service/message").Start(ctx, "MessageService.SendMessage",
telemetry.Int64("user_id", p.UserID),
telemetry.Int64("channel_id", p.ChannelID),
)
+14 -13
View File
@@ -1,6 +1,7 @@
package service
import (
"context"
"errors"
"strings"
"testing"
@@ -34,7 +35,7 @@ func newTestMessageService() (*MessageService, *store.MemStore) {
func TestSendMessage_Valid(t *testing.T) {
svc, _ := newTestMessageService()
result, err := svc.SendMessage(SendMessageParams{
result, err := svc.SendMessage(context.Background(), SendMessageParams{
ChannelID: 10,
UserID: 1,
Username: "alice",
@@ -58,7 +59,7 @@ func TestSendMessage_Valid(t *testing.T) {
func TestSendMessage_EmptyContent(t *testing.T) {
svc, _ := newTestMessageService()
_, err := svc.SendMessage(SendMessageParams{
_, err := svc.SendMessage(context.Background(), SendMessageParams{
ChannelID: 10,
UserID: 1,
Username: "alice",
@@ -79,7 +80,7 @@ func TestSendMessage_ExceedsMaxLength(t *testing.T) {
// maxMessageLen is 4000 runes; create content that exceeds it.
longContent := strings.Repeat("a", 4001)
_, err := svc.SendMessage(SendMessageParams{
_, err := svc.SendMessage(context.Background(), SendMessageParams{
ChannelID: 10,
UserID: 1,
Username: "alice",
@@ -97,7 +98,7 @@ func TestSendMessage_ExceedsMaxLength(t *testing.T) {
func TestSendMessage_ChannelNotFound(t *testing.T) {
svc, _ := newTestMessageService()
_, err := svc.SendMessage(SendMessageParams{
_, err := svc.SendMessage(context.Background(), SendMessageParams{
ChannelID: 999,
UserID: 1,
Username: "alice",
@@ -114,7 +115,7 @@ func TestSendMessage_ChannelNotFound(t *testing.T) {
func TestSendMessage_InvalidChannelID(t *testing.T) {
svc, _ := newTestMessageService()
_, err := svc.SendMessage(SendMessageParams{
_, err := svc.SendMessage(context.Background(), SendMessageParams{
ChannelID: 0,
UserID: 1,
Username: "alice",
@@ -145,7 +146,7 @@ func TestSendMessage_NoPermission(t *testing.T) {
permSvc := NewPermissionService(ms, checker)
svc := NewMessageService(ms, permSvc, nil)
_, err := svc.SendMessage(SendMessageParams{
_, err := svc.SendMessage(context.Background(), SendMessageParams{
ChannelID: 10,
UserID: 1,
Username: "alice",
@@ -163,7 +164,7 @@ func TestEditMessage_OwnerCanEdit(t *testing.T) {
svc, _ := newTestMessageService()
// Send a message first.
result, err := svc.SendMessage(SendMessageParams{
result, err := svc.SendMessage(context.Background(), SendMessageParams{
ChannelID: 10,
UserID: 1,
Username: "alice",
@@ -192,7 +193,7 @@ func TestEditMessage_NonOwnerFails(t *testing.T) {
ms.SeedUser(&db.User{ID: 2, Username: "bob"})
// User 1 sends a message.
result, err := svc.SendMessage(SendMessageParams{
result, err := svc.SendMessage(context.Background(), SendMessageParams{
ChannelID: 10,
UserID: 1,
Username: "alice",
@@ -215,7 +216,7 @@ func TestEditMessage_NonOwnerFails(t *testing.T) {
func TestEditMessage_EmptyContentFails(t *testing.T) {
svc, _ := newTestMessageService()
result, err := svc.SendMessage(SendMessageParams{
result, err := svc.SendMessage(context.Background(), SendMessageParams{
ChannelID: 10,
UserID: 1,
Username: "alice",
@@ -237,7 +238,7 @@ func TestEditMessage_EmptyContentFails(t *testing.T) {
func TestDeleteMessage_OwnerCanDelete(t *testing.T) {
svc, _ := newTestMessageService()
result, err := svc.SendMessage(SendMessageParams{
result, err := svc.SendMessage(context.Background(), SendMessageParams{
ChannelID: 10,
UserID: 1,
Username: "alice",
@@ -265,7 +266,7 @@ func TestDeleteMessage_NonOwnerWithoutModFails(t *testing.T) {
ms.SeedUser(&db.User{ID: 2, Username: "bob"})
// User 1 sends.
result, err := svc.SendMessage(SendMessageParams{
result, err := svc.SendMessage(context.Background(), SendMessageParams{
ChannelID: 10,
UserID: 1,
Username: "alice",
@@ -312,7 +313,7 @@ func TestDeleteMessage_ModCanDeleteOthersMessage(t *testing.T) {
svc := NewMessageService(ms, permSvc, nil)
// User 1 sends a message.
result, err := svc.SendMessage(SendMessageParams{
result, err := svc.SendMessage(context.Background(), SendMessageParams{
ChannelID: 10,
UserID: 1,
Username: "alice",
@@ -347,7 +348,7 @@ func TestDeleteMessage_InvalidMessageID(t *testing.T) {
func TestSendMessage_HTMLSanitized(t *testing.T) {
svc, _ := newTestMessageService()
result, err := svc.SendMessage(SendMessageParams{
result, err := svc.SendMessage(context.Background(), SendMessageParams{
ChannelID: 10,
UserID: 1,
Username: "alice",
+2 -2
View File
@@ -24,8 +24,8 @@ func NewUserService(st store.Store) *UserService {
// UpdateProfile updates a user's username and/or avatar.
// Returns the updated user for response building.
func (s *UserService) UpdateProfile(userID int64, username string, avatar *string) (*db.User, error) {
ctx, span := telemetry.GlobalTracer("service/user").Start(context.Background(), "UserService.UpdateProfile",
func (s *UserService) UpdateProfile(ctx context.Context, userID int64, username string, avatar *string) (*db.User, error) {
ctx, span := telemetry.GlobalTracer("service/user").Start(ctx, "UserService.UpdateProfile",
telemetry.Int64("user_id", userID),
)
start := time.Now()
+5 -5
View File
@@ -40,7 +40,7 @@ func (s *SQLiteStore) GetEventsSince(ctx context.Context, afterSeq int64, limit
if err != nil {
return nil, fmt.Errorf("GetEventsSince: %w", err)
}
defer rows.Close()
defer func() { _ = rows.Close() }()
return scanEventRows(rows)
}
@@ -61,7 +61,7 @@ func (s *SQLiteStore) GetEventsSinceForChannels(ctx context.Context, afterSeq in
if err != nil {
return nil, fmt.Errorf("GetEventsSinceForChannels (global only): %w", err)
}
defer rows.Close()
defer func() { _ = rows.Close() }()
return scanEventRows(rows)
}
@@ -87,7 +87,7 @@ func (s *SQLiteStore) GetEventsSinceForChannels(ctx context.Context, afterSeq in
if err != nil {
return nil, fmt.Errorf("GetEventsSinceForChannels: %w", err)
}
defer rows.Close()
defer func() { _ = rows.Close() }()
return scanEventRows(rows)
}
@@ -180,7 +180,7 @@ func (s *SQLiteStore) ListPlugins(ctx context.Context) ([]db.PluginRow, error) {
if err != nil {
return nil, fmt.Errorf("ListPlugins: %w", err)
}
defer rows.Close()
defer func() { _ = rows.Close() }()
var out []db.PluginRow
for rows.Next() {
var p db.PluginRow
@@ -233,7 +233,7 @@ func (s *SQLiteStore) PluginKVScan(ctx context.Context, pluginID int64, prefix s
if err != nil {
return nil, fmt.Errorf("PluginKVScan: %w", err)
}
defer rows.Close()
defer func() { _ = rows.Close() }()
out := make(map[string][]byte)
for rows.Next() {
var k string
+2 -2
View File
@@ -15,11 +15,11 @@ func registerChatHandlers(r *HandlerRegistry, deps ChatDeps) {
}
// handleChatSendV2 processes a chat_send command via the MessageService.
func handleChatSendV2(_ context.Context, cmd Command, info ClientInfo, deps any) Result {
func handleChatSendV2(ctx context.Context, cmd Command, info ClientInfo, deps any) Result {
d := deps.(ChatDeps)
sendCmd := cmd.(ChatSendCmd)
result, err := d.MessageSvc.SendMessage(service.SendMessageParams{
result, err := d.MessageSvc.SendMessage(ctx, service.SendMessageParams{
ChannelID: sendCmd.ChannelID(),
UserID: info.UserID,
Username: info.Username,