Skip to content

Commit 115fa29

Browse files
committed
fix: overflow-safe span checks and goroutine recover in buildResult
1 parent 53eb750 commit 115fa29

2 files changed

Lines changed: 27 additions & 12 deletions

File tree

apps/api/services/text/spellcheck/service.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,11 @@ func (s *Service) CheckBatch(texts []string) []Result {
9292
go func(i int, text string) {
9393
defer wg.Done()
9494
defer func() { <-sem }()
95+
defer func() {
96+
if rec := recover(); rec != nil {
97+
results[i] = Result{}
98+
}
99+
}()
95100

96101
r, err := s.Check(text)
97102
if err != nil {
@@ -106,14 +111,24 @@ func (s *Service) CheckBatch(texts []string) []Result {
106111
return results
107112
}
108113

114+
// isValidSpan reports whether m describes a non-empty, non-overflowing span
115+
// within a slice of runeCount runes.
116+
func isValidSpan(m ltMatch, runeCount int) bool {
117+
return len(m.Replacements) > 0 &&
118+
m.Offset >= 0 &&
119+
m.Length >= 0 &&
120+
m.Offset < runeCount &&
121+
m.Length <= runeCount-m.Offset
122+
}
123+
109124
// buildResult converts LanguageTool matches into our Result type.
110125
// Replacements are applied in reverse order to keep offsets valid.
111126
func buildResult(text string, matches []ltMatch) Result {
112127
runes := []rune(text)
113128
corrections := make([]Correction, 0, len(matches))
114129

115130
for _, m := range matches {
116-
if len(m.Replacements) == 0 || m.Offset < 0 || m.Length < 0 || m.Offset+m.Length > len(runes) {
131+
if !isValidSpan(m, len(runes)) {
117132
continue
118133
}
119134
// Collect up to 3 suggestions so callers can present a picker.
@@ -137,7 +152,7 @@ func buildResult(text string, matches []ltMatch) Result {
137152
copy(corrected, runes)
138153
for i := len(matches) - 1; i >= 0; i-- {
139154
m := matches[i]
140-
if len(m.Replacements) == 0 || m.Offset < 0 || m.Length < 0 || m.Offset+m.Length > len(corrected) {
155+
if !isValidSpan(m, len(corrected)) {
141156
continue
142157
}
143158
repl := []rune(m.Replacements[0].Value)

docs/apis/text/spell-check.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -133,19 +133,19 @@ Each text in the batch counts as **1 unit** of quota. A request with 10 texts co
133133

134134
### Partial failure
135135

136-
This endpoint always returns `200` with a result for every input text. Up to 10
137-
texts are checked concurrently. If LanguageTool is unreachable for an individual
138-
text, that slot returns a zero-value result (`corrected: ""`, `corrections: null`)
139-
and the batch continues — the caller receives a full-length `results` array
140-
regardless of individual failures.
136+
Once validation passes, this endpoint returns `200` with a result for every input
137+
text. Up to 10 texts are checked concurrently. If LanguageTool is unreachable for
138+
an individual text, that slot returns a zero-value result
139+
(`corrected: ""`, `corrections: null`) and the batch continues — the caller
140+
receives a full-length `results` array regardless of individual failures.
141141

142142
### Batch Error Codes
143143

144-
| Code | Status | When |
145-
| ------------------- | ------ | ---------------------------------------------------------- |
146-
| `validation_failed` | 422 | `texts` is missing, empty, or contains more than 50 items |
147-
| `bad_request` | 400 | Request body is missing or not JSON |
148-
| `internal_error` | 500 | Unexpected failure |
144+
| Code | Status | When |
145+
| ------------------- | ------ | ---------------------------------------------------------------------------- |
146+
| `validation_failed` | 422 | `texts` is missing, empty, or contains more than 50 items |
147+
| `bad_request` | 400 | Request body is missing or not JSON |
148+
| `internal_error` | 500 | Infrastructure-level failure before processing begins (e.g. handler error) |
149149

150150
---
151151

0 commit comments

Comments
 (0)