Skip to content

Commit 95065c7

Browse files
authored
Merge pull request #247 from x1unix/release/v1.13.2
Release: v1.13.2
2 parents 8aab6d3 + 182a2b0 commit 95065c7

File tree

9 files changed

+107
-82
lines changed

9 files changed

+107
-82
lines changed

internal/langserver/errors.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@ package langserver
33
import (
44
"fmt"
55
"net/http"
6+
7+
"github.com/x1unix/go-playground/pkg/goplay"
8+
)
9+
10+
// ErrSnippetTooLarge is snippet max size limit error
11+
var ErrSnippetTooLarge = Errorf(
12+
http.StatusRequestEntityTooLarge,
13+
"code snippet too large (max %d bytes)",
14+
goplay.MaxSnippetSize,
615
)
716

817
// HTTPError is HTTP response error

internal/langserver/middleware.go

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package langserver
22

33
import (
44
"errors"
5-
"github.com/x1unix/go-playground/pkg/goplay"
65
"net/http"
76
"syscall"
87
)
@@ -29,15 +28,6 @@ func WrapHandler(h HandlerFunc, guards ...GuardFn) http.HandlerFunc {
2928
}
3029
}
3130

32-
// ValidateContentLength validates Go code snippet size
33-
func ValidateContentLength(r *http.Request) error {
34-
if err := goplay.ValidateContentLength(int(r.ContentLength)); err != nil {
35-
return NewHTTPError(http.StatusRequestEntityTooLarge, err)
36-
}
37-
38-
return nil
39-
}
40-
4131
func handleError(err error, w http.ResponseWriter) {
4232
if err == nil {
4333
return

internal/langserver/request.go

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package langserver
33
import (
44
"encoding/json"
55
"go.uber.org/zap"
6-
"io"
76
"net/http"
87
"strconv"
98
)
@@ -60,13 +59,3 @@ func shouldFormatCode(r *http.Request) (bool, error) {
6059

6160
return boolVal, nil
6261
}
63-
64-
func getPayloadFromRequest(r *http.Request) ([]byte, error) {
65-
src, err := io.ReadAll(r.Body)
66-
if err != nil {
67-
return nil, Errorf(http.StatusBadGateway, "failed to read request: %s", err)
68-
}
69-
70-
r.Body.Close()
71-
return src, nil
72-
}

internal/langserver/server.go

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package langserver
22

33
import (
4+
"bytes"
45
"context"
56
"errors"
67
"fmt"
@@ -70,13 +71,13 @@ func (s *Service) Mount(r *mux.Router) {
7071
r.Path("/suggest").
7172
HandlerFunc(WrapHandler(s.HandleGetSuggestion))
7273
r.Path("/run").Methods(http.MethodPost).
73-
HandlerFunc(WrapHandler(s.HandleRunCode, ValidateContentLength))
74+
HandlerFunc(WrapHandler(s.HandleRunCode))
7475
r.Path("/compile").Methods(http.MethodPost).
75-
HandlerFunc(WrapHandler(s.HandleCompile, ValidateContentLength))
76+
HandlerFunc(WrapHandler(s.HandleCompile))
7677
r.Path("/format").Methods(http.MethodPost).
77-
HandlerFunc(WrapHandler(s.HandleFormatCode, ValidateContentLength))
78+
HandlerFunc(WrapHandler(s.HandleFormatCode))
7879
r.Path("/share").Methods(http.MethodPost).
79-
HandlerFunc(WrapHandler(s.HandleShare, ValidateContentLength))
80+
HandlerFunc(WrapHandler(s.HandleShare))
8081
r.Path("/snippet/{id}").Methods(http.MethodGet).
8182
HandlerFunc(WrapHandler(s.HandleGetSnippet))
8283
r.Path("/backends/info").Methods(http.MethodGet).
@@ -159,7 +160,7 @@ func (s *Service) HandleGetSuggestion(w http.ResponseWriter, r *http.Request) er
159160

160161
// HandleFormatCode handles goimports action
161162
func (s *Service) HandleFormatCode(w http.ResponseWriter, r *http.Request) error {
162-
src, err := getPayloadFromRequest(r)
163+
src, err := s.getPayloadFromRequest(r)
163164
if err != nil {
164165
return err
165166
}
@@ -186,8 +187,8 @@ func (s *Service) HandleShare(w http.ResponseWriter, r *http.Request) error {
186187
shareID, err := s.client.Share(r.Context(), r.Body)
187188
defer r.Body.Close()
188189
if err != nil {
189-
if errors.Is(err, goplay.ErrSnippetTooLarge) {
190-
return NewHTTPError(http.StatusRequestEntityTooLarge, err)
190+
if isContentLengthError(err) {
191+
return ErrSnippetTooLarge
191192
}
192193

193194
s.log.Error("failed to share code: ", err)
@@ -225,7 +226,7 @@ func (s *Service) HandleGetSnippet(w http.ResponseWriter, r *http.Request) error
225226
// HandleRunCode handles code run
226227
func (s *Service) HandleRunCode(w http.ResponseWriter, r *http.Request) error {
227228
ctx := r.Context()
228-
src, err := getPayloadFromRequest(r)
229+
src, err := s.getPayloadFromRequest(r)
229230
if err != nil {
230231
return err
231232
}
@@ -304,17 +305,15 @@ func (s *Service) HandleArtifactRequest(w http.ResponseWriter, r *http.Request)
304305
w.Header().Set("Content-Length", contentLength)
305306
w.Header().Set(rawContentLengthHeader, contentLength)
306307

307-
n, err := io.Copy(w, data)
308308
defer data.Close()
309-
if err != nil {
309+
if _, err := io.Copy(w, data); err != nil {
310310
s.log.Errorw("failed to send artifact",
311311
"artifactID", artifactId,
312312
"err", err,
313313
)
314314
return err
315315
}
316316

317-
w.Header().Set("Content-Length", strconv.FormatInt(n, 10))
318317
return nil
319318
}
320319

@@ -329,7 +328,7 @@ func (s *Service) HandleCompile(w http.ResponseWriter, r *http.Request) error {
329328
return NewHTTPError(http.StatusTooManyRequests, err)
330329
}
331330

332-
src, err := getPayloadFromRequest(r)
331+
src, err := s.getPayloadFromRequest(r)
333332
if err != nil {
334333
return err
335334
}
@@ -391,8 +390,8 @@ func backendFromRequest(r *http.Request) (goplay.Backend, error) {
391390
func (s *Service) goImportsCode(ctx context.Context, src []byte, backend goplay.Backend) ([]byte, bool, error) {
392391
resp, err := s.client.GoImports(ctx, src, backend)
393392
if err != nil {
394-
if errors.Is(err, goplay.ErrSnippetTooLarge) {
395-
return nil, false, NewHTTPError(http.StatusRequestEntityTooLarge, err)
393+
if isContentLengthError(err) {
394+
return nil, false, ErrSnippetTooLarge
396395
}
397396

398397
s.log.Error(err)
@@ -406,3 +405,31 @@ func (s *Service) goImportsCode(ctx context.Context, src []byte, backend goplay.
406405
changed := resp.Body != string(src)
407406
return []byte(resp.Body), changed, nil
408407
}
408+
409+
func (s *Service) getPayloadFromRequest(r *http.Request) ([]byte, error) {
410+
// see: https://github.com/golang/playground/blob/master/share.go#L69
411+
var buff bytes.Buffer
412+
buff.Grow(goplay.MaxSnippetSize)
413+
414+
defer r.Body.Close()
415+
_, err := io.Copy(&buff, io.LimitReader(r.Body, goplay.MaxSnippetSize+1))
416+
if err != nil {
417+
return nil, Errorf(http.StatusBadGateway, "failed to read request: %w", err)
418+
}
419+
420+
if buff.Len() > goplay.MaxSnippetSize {
421+
return nil, ErrSnippetTooLarge
422+
}
423+
424+
return buff.Bytes(), nil
425+
}
426+
427+
func isContentLengthError(err error) bool {
428+
if httpErr, ok := goplay.IsHTTPError(err); ok {
429+
if httpErr.StatusCode == http.StatusRequestEntityTooLarge {
430+
return true
431+
}
432+
}
433+
434+
return false
435+
}

pkg/goplay/client.go

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,11 @@ const (
1717
DefaultUserAgent = "goplay.tools/1.0 (http://goplay.tools/)"
1818
DefaultPlaygroundURL = "https://go.dev/_"
1919

20-
// maxSnippetSize value taken from
20+
// MaxSnippetSize value taken from
2121
// https://github.com/golang/playground/blob/master/app/goplay/share.go
22-
maxSnippetSize = 64 * 1024
22+
MaxSnippetSize = 64 * 1024
2323
)
2424

25-
// ErrSnippetTooLarge is snippet max size limit error
26-
var ErrSnippetTooLarge = fmt.Errorf("code snippet too large (max %d bytes)", maxSnippetSize)
27-
2825
// Client is Go Playground API client
2926
type Client struct {
3027
client http.Client
@@ -89,14 +86,11 @@ func (c *Client) doRequest(ctx context.Context, method, url, contentType string,
8986
return nil, NewHTTPError(response)
9087
}
9188

92-
bodyBytes := &bytes.Buffer{}
93-
_, err = io.Copy(bodyBytes, io.LimitReader(response.Body, maxSnippetSize+1))
89+
bodyBytes := bytes.Buffer{}
90+
_, err = io.Copy(&bodyBytes, response.Body)
9491
if err != nil {
9592
return nil, err
9693
}
97-
if err = ValidateContentLength(bodyBytes.Len()); err != nil {
98-
return nil, err
99-
}
10094

10195
return bodyBytes.Bytes(), nil
10296
}

pkg/goplay/methods.go

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,6 @@ import (
88
"net/url"
99
)
1010

11-
// ValidateContentLength validates snippet size
12-
func ValidateContentLength(itemLen int) error {
13-
if itemLen > maxSnippetSize {
14-
return ErrSnippetTooLarge
15-
}
16-
return nil
17-
}
18-
1911
// GetSnippet returns snippet from Go playground
2012
func (c *Client) GetSnippet(ctx context.Context, snippetID string) (*Snippet, error) {
2113
fileName := snippetID + ".go"

pkg/goplay/methods_test.go

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"net/http"
88
"net/http/httptest"
99
"net/url"
10-
"strconv"
1110
"testing"
1211
"time"
1312

@@ -16,24 +15,6 @@ import (
1615
"github.com/x1unix/go-playground/pkg/testutil"
1716
)
1817

19-
func TestValidateContentLength(t *testing.T) {
20-
cases := map[int]bool{
21-
maxSnippetSize: false,
22-
maxSnippetSize + 10: true,
23-
10: false,
24-
}
25-
for i, c := range cases {
26-
t.Run(strconv.Itoa(i), func(t *testing.T) {
27-
err := ValidateContentLength(i)
28-
if !c {
29-
require.NoError(t, err)
30-
return
31-
}
32-
require.Error(t, err)
33-
})
34-
}
35-
}
36-
3718
func TestClient_Compile(t *testing.T) {
3819
cases := map[string]struct {
3920
expect *CompileResponse

web/src/store/dispatchers/build.ts

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {TargetType} from '~/services/config';
22
import {getWorkerInstance} from "~/services/gorepl";
33
import {getImportObject, goRun} from '~/services/go';
4-
import {setTimeoutNanos} from "~/utils/duration";
4+
import {setTimeoutNanos, SECOND} from "~/utils/duration";
55
import client, {
66
EvalEvent,
77
EvalEventKind,
@@ -29,6 +29,29 @@ import {wrapResponseWithProgress} from "~/utils/http";
2929
const WASM_APP_DOWNLOAD_NOTIFICATION = 'WASM_APP_DOWNLOAD_NOTIFICATION';
3030
const WASM_APP_EXIT_ERROR = 'WASM_APP_EXIT_ERROR';
3131

32+
/**
33+
* Go program execution timeout in nanoseconds
34+
*/
35+
const runTimeoutNs = 5 * SECOND;
36+
37+
const lastElem = <T>(items: T[]): T|undefined => (
38+
items?.slice(-1)?.[0]
39+
);
40+
41+
const hasProgramTimeoutError = (events: EvalEvent[]) => {
42+
if (!events.length) {
43+
return false;
44+
}
45+
46+
const { Message, Kind } = events[0];
47+
if (Kind === 'stderr' && Message.trim() === 'timeout running program') {
48+
const lastEvent = lastElem(events);
49+
return lastEvent!.Delay >= runTimeoutNs;
50+
}
51+
52+
return false;
53+
}
54+
3255
const dispatchEvalEvents = (dispatch: DispatchFn, events: EvalEvent[]) => {
3356
// TODO: support cancellation
3457

@@ -39,20 +62,35 @@ const dispatchEvalEvents = (dispatch: DispatchFn, events: EvalEvent[]) => {
3962

4063
// Each eval event contains time since previous event.
4164
// Convert relative delay into absolute delay since program start.
42-
const eventsWithDelay = events.map((event, i, arr) => (
43-
i === 0 ? event : (
44-
{
45-
...event,
46-
Delay: arr[i - 1].Delay + event.Delay
47-
}
48-
)
49-
));
65+
let eventsWithDelay = events
66+
.reduce((accum: EvalEvent[], {Delay: delay, ...item}) => (
67+
[
68+
...accum,
69+
{
70+
...item,
71+
Delay: (lastElem(accum)?.Delay ?? 0) + delay,
72+
}
73+
]
74+
), []);
75+
76+
// Sometimes Go playground fails to detect execution timeout error and still sends all events.
77+
// This dirty hack attempts to normalize this case.
78+
if (hasProgramTimeoutError(eventsWithDelay)) {
79+
eventsWithDelay = eventsWithDelay
80+
.slice(1)
81+
.filter(({Delay}) => Delay <= runTimeoutNs)
82+
.concat({
83+
Kind: EvalEventKind.Stderr,
84+
Message: `Go program execution timeout exceeded (max: ${runTimeoutNs / SECOND}s)`,
85+
Delay: runTimeoutNs,
86+
});
87+
}
5088

5189
// Try to guess program end time by checking last message delay.
5290
//
5391
// This won't work if "time.Sleep()" occurs after final message but the same
5492
// approach used in official playground, so should be enough for us.
55-
const programEndTime = eventsWithDelay?.slice(-1)?.[0]?.Delay ?? 0;
93+
let programEndTime = lastElem(eventsWithDelay)?.Delay ?? 0;
5694

5795
dispatch(newProgramStartAction());
5896
eventsWithDelay.forEach(event => {

web/src/utils/duration.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
*/
44
export const MSEC_IN_NANOSEC = 1000000;
55

6+
/**
7+
* Number of nanoseconds in a second.
8+
*/
9+
export const SECOND = 1000 * MSEC_IN_NANOSEC;
10+
611
/**
712
* Converts nanoseconds to milliseconds
813
* @param ns Delay in anoseconds

0 commit comments

Comments
 (0)