From 60aeabcd6c3739d56143339b67632dc6658c0ce7 Mon Sep 17 00:00:00 2001 From: Indent Date: Thu, 29 Oct 2026 08:02:50 +0000 Subject: [PATCH] connector: keep contact search bounded and stop it when the request is canceled SearchUsers looked up every matching contact again one by one, returned every match, and kept going after the client went away. The provisioning API then synced each result's ghost, downloading avatars with a client that ignored the request context, so a short query on a large contact list ran for minutes. Cache the contacts from the batch lookup, cap name matches at 20, stop between batches once the context is done, and make avatar downloads use the request context and reject non-200 responses. --- pkg/connector/userinfo.go | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/pkg/connector/userinfo.go b/pkg/connector/userinfo.go index 15d8fdbf..1ea9bea3 100644 --- a/pkg/connector/userinfo.go +++ b/pkg/connector/userinfo.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "io" + "net/http" "strings" "time" @@ -341,6 +342,8 @@ func (lc *LineClient) midToResolveIdentifier(ctx context.Context, mid string) *b } } +const maxSearchResults = 20 + func (lc *LineClient) SearchUsers(ctx context.Context, query string) ([]*bridgev2.ResolveIdentifierResponse, error) { var results []*bridgev2.ResolveIdentifierResponse @@ -353,9 +356,6 @@ func (lc *LineClient) SearchUsers(ctx context.Context, query string) ([]*bridgev if err == nil && contact != nil && contact.Mid != "" { if r := lc.midToResolveIdentifier(ctx, contact.Mid); r != nil { results = append(results, r) - if len(results) >= 20 { - return results, nil - } } } } @@ -370,7 +370,10 @@ func (lc *LineClient) SearchUsers(ctx context.Context, query string) ([]*bridgev } // Fetch contacts in batches to check display names - for i := 0; i < len(allMids); i += 50 { + for i := 0; i < len(allMids) && len(results) < maxSearchResults; i += 50 { + if err := ctx.Err(); err != nil { + return results, err + } end := i + 50 if end > len(allMids) { end = len(allMids) @@ -385,10 +388,14 @@ func (lc *LineClient) SearchUsers(ctx context.Context, query string) ([]*bridgev if c.Mid == "" { continue } + lc.setCachedContact(c.Mid, c) name := strings.ToLower(c.EffectiveDisplayName()) if lowerQuery == "" || strings.Contains(name, lowerQuery) { if r := lc.midToResolveIdentifier(ctx, c.Mid); r != nil { results = append(results, r) + if len(results) >= maxSearchResults { + break + } } } } @@ -409,6 +416,9 @@ func (lc *LineClient) GetContactList(ctx context.Context) ([]*bridgev2.ResolveId var results []*bridgev2.ResolveIdentifierResponse for i := 0; i < len(allMids); i += 50 { + if err := ctx.Err(); err != nil { + return nil, err + } end := i + 50 if end > len(allMids) { end = len(allMids) @@ -422,6 +432,7 @@ func (lc *LineClient) GetContactList(ctx context.Context) ([]*bridgev2.ResolveId if wrapper.Contact.Mid == "" { continue } + lc.setCachedContact(mid, wrapper.Contact) if r := lc.midToResolveIdentifier(ctx, mid); r != nil { results = append(results, r) } @@ -434,11 +445,17 @@ func (lc *LineClient) GetContactList(ctx context.Context) ([]*bridgev2.ResolveId var _ bridgev2.ContactListingNetworkAPI = (*LineClient)(nil) func (lc *LineClient) GetAvatar(ctx context.Context, id networkid.AvatarID) ([]byte, error) { - url := fmt.Sprintf("https://profile.line-scdn.net%s", id) - resp, err := lc.HTTPClient.Get(url) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://profile.line-scdn.net%s", id), nil) + if err != nil { + return nil, err + } + resp, err := lc.HTTPClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected avatar response status %d", resp.StatusCode) + } return io.ReadAll(resp.Body) }