@@ -3,10 +3,15 @@ package buffer
33import (
44 "bufio"
55 "fmt"
6+ "io"
67 "net/http"
78 "strings"
89)
910
11+ // maxLineSize is the maximum size for a single log line (10MB).
12+ // GitHub Actions logs can contain extremely long lines (base64 content, minified JS, etc.)
13+ const maxLineSize = 10 * 1024 * 1024
14+
1015// ProcessResponseAsRingBufferToEnd reads the body of an HTTP response line by line,
1116// storing only the last maxJobLogLines lines using a ring buffer (sliding window).
1217// This efficiently retains the most recent lines, overwriting older ones as needed.
@@ -25,6 +30,7 @@ import (
2530//
2631// The function uses a ring buffer to efficiently store only the last maxJobLogLines lines.
2732// If the response contains more lines than maxJobLogLines, only the most recent lines are kept.
33+ // Lines exceeding maxLineSize are truncated with a marker.
2834func ProcessResponseAsRingBufferToEnd (httpResp * http.Response , maxJobLogLines int ) (string , int , * http.Response , error ) {
2935 if maxJobLogLines > 100000 {
3036 maxJobLogLines = 100000
@@ -36,7 +42,8 @@ func ProcessResponseAsRingBufferToEnd(httpResp *http.Response, maxJobLogLines in
3642 writeIndex := 0
3743
3844 scanner := bufio .NewScanner (httpResp .Body )
39- scanner .Buffer (make ([]byte , 0 , 64 * 1024 ), 1024 * 1024 )
45+ // Set initial buffer to 64KB and max token size to 10MB to handle very long lines
46+ scanner .Buffer (make ([]byte , 0 , 64 * 1024 ), maxLineSize )
4047
4148 for scanner .Scan () {
4249 line := scanner .Text ()
@@ -48,6 +55,11 @@ func ProcessResponseAsRingBufferToEnd(httpResp *http.Response, maxJobLogLines in
4855 }
4956
5057 if err := scanner .Err (); err != nil {
58+ // If we hit a token too long error, fall back to byte-by-byte reading
59+ // with line truncation to handle extremely long lines gracefully
60+ if err == bufio .ErrTooLong {
61+ return processWithLongLineHandling (httpResp .Body , lines , validLines , totalLines , writeIndex , maxJobLogLines )
62+ }
5163 return "" , 0 , httpResp , fmt .Errorf ("failed to read log content: %w" , err )
5264 }
5365
@@ -71,3 +83,75 @@ func ProcessResponseAsRingBufferToEnd(httpResp *http.Response, maxJobLogLines in
7183
7284 return strings .Join (result , "\n " ), totalLines , httpResp , nil
7385}
86+
87+ // processWithLongLineHandling continues processing after encountering a line
88+ // that exceeds the scanner's max token size. It reads byte-by-byte and
89+ // truncates extremely long lines instead of failing.
90+ func processWithLongLineHandling (body io.Reader , lines []string , validLines []bool , totalLines , writeIndex , maxJobLogLines int ) (string , int , * http.Response , error ) {
91+ // Add a marker that we encountered truncated content
92+ truncatedMarker := "[LINE TRUNCATED - exceeded maximum line length of 10MB]"
93+ lines [writeIndex ] = truncatedMarker
94+ validLines [writeIndex ] = true
95+ totalLines ++
96+ writeIndex = (writeIndex + 1 ) % maxJobLogLines
97+
98+ // Continue reading with a buffered reader, truncating long lines
99+ reader := bufio .NewReader (body )
100+ var currentLine strings.Builder
101+ const maxDisplayLength = 1000 // Keep first 1000 chars of truncated lines
102+
103+ for {
104+ b , err := reader .ReadByte ()
105+ if err == io .EOF {
106+ // Handle final line without newline
107+ if currentLine .Len () > 0 {
108+ line := currentLine .String ()
109+ if len (line ) > maxLineSize {
110+ line = line [:maxDisplayLength ] + "... [TRUNCATED]"
111+ }
112+ lines [writeIndex ] = line
113+ validLines [writeIndex ] = true
114+ totalLines ++
115+ }
116+ break
117+ }
118+ if err != nil {
119+ return "" , 0 , nil , fmt .Errorf ("failed to read log content: %w" , err )
120+ }
121+
122+ if b == '\n' {
123+ line := currentLine .String ()
124+ if len (line ) > maxLineSize {
125+ line = line [:maxDisplayLength ] + "... [TRUNCATED]"
126+ }
127+ lines [writeIndex ] = line
128+ validLines [writeIndex ] = true
129+ totalLines ++
130+ writeIndex = (writeIndex + 1 ) % maxJobLogLines
131+ currentLine .Reset ()
132+ } else if currentLine .Len () < maxLineSize + maxDisplayLength {
133+ // Stop accumulating bytes once we exceed the limit (plus buffer for truncation message)
134+ currentLine .WriteByte (b )
135+ }
136+ }
137+
138+ var result []string
139+ linesInBuffer := totalLines
140+ if linesInBuffer > maxJobLogLines {
141+ linesInBuffer = maxJobLogLines
142+ }
143+
144+ startIndex := 0
145+ if totalLines > maxJobLogLines {
146+ startIndex = writeIndex
147+ }
148+
149+ for i := 0 ; i < linesInBuffer ; i ++ {
150+ idx := (startIndex + i ) % maxJobLogLines
151+ if validLines [idx ] {
152+ result = append (result , lines [idx ])
153+ }
154+ }
155+
156+ return strings .Join (result , "\n " ), totalLines , nil , nil
157+ }
0 commit comments