Skip to content

Commit 3abda4f

Browse files
feat: handle aborted connection (#95)
* Handle aborted connection * Handle when writing as well * return bytes written * optimize return * remove goroutine * fix style * Add tests * add missing newline
1 parent 8e136d0 commit 3abda4f

4 files changed

Lines changed: 144 additions & 8 deletions

File tree

frankenphp.c

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,13 @@ static size_t frankenphp_ub_write(const char *str, size_t str_length)
405405
return 0;
406406
}
407407

408-
return go_ub_write(ctx->current_request ? ctx->current_request : ctx->main_request, (char *) str, str_length);
408+
struct go_ub_write_return result = go_ub_write(ctx->current_request ? ctx->current_request : ctx->main_request, (char *) str, str_length);
409+
410+
if (result.r1) {
411+
php_handle_aborted_connection();
412+
}
413+
414+
return result.r0;
409415
}
410416

411417
static int frankenphp_send_headers(sapi_headers_struct *sapi_headers)
@@ -445,7 +451,7 @@ static void frankenphp_sapi_flush(void *server_context)
445451

446452
if (!ctx || ctx->current_request == 0) return;
447453

448-
go_sapi_flush(ctx->current_request);
454+
if (go_sapi_flush(ctx->current_request)) php_handle_aborted_connection();
449455
}
450456

451457
static size_t frankenphp_read_post(char *buffer, size_t count_bytes)

frankenphp.go

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -124,13 +124,22 @@ type FrankenPHPContext struct {
124124
populated bool
125125
authPassword string
126126

127-
// Whether the request is already closed
127+
// Whether the request is already closed by us
128128
closed sync.Once
129129

130130
responseWriter http.ResponseWriter
131131
done chan interface{}
132132
}
133133

134+
func clientHasClosed(r *http.Request) bool {
135+
select {
136+
case <-r.Context().Done():
137+
return true
138+
default:
139+
return false
140+
}
141+
}
142+
134143
// NewRequestWithContext creates a new FrankenPHP request context.
135144
func NewRequestWithContext(r *http.Request, documentRoot string, l *zap.Logger) *http.Request {
136145
if l == nil {
@@ -407,7 +416,7 @@ func go_execute_script(rh unsafe.Pointer) {
407416
}
408417

409418
//export go_ub_write
410-
func go_ub_write(rh C.uintptr_t, cString *C.char, length C.int) C.size_t {
419+
func go_ub_write(rh C.uintptr_t, cString *C.char, length C.int) (C.size_t, C.bool) {
411420
r := cgo.Handle(rh).Value().(*http.Request)
412421
fc, _ := FromContext(r.Context())
413422

@@ -426,7 +435,7 @@ func go_ub_write(rh C.uintptr_t, cString *C.char, length C.int) C.size_t {
426435
fc.Logger.Info(writer.(*bytes.Buffer).String())
427436
}
428437

429-
return C.size_t(i)
438+
return C.size_t(i), C.bool(clientHasClosed(r))
430439
}
431440

432441
//export go_register_variables
@@ -486,20 +495,26 @@ func go_write_header(rh C.uintptr_t, status C.int) {
486495
}
487496

488497
//export go_sapi_flush
489-
func go_sapi_flush(rh C.uintptr_t) {
498+
func go_sapi_flush(rh C.uintptr_t) bool {
490499
r := cgo.Handle(rh).Value().(*http.Request)
491500
fc := r.Context().Value(contextKey).(*FrankenPHPContext)
492501

493502
if fc.responseWriter == nil {
494-
return
503+
return true
495504
}
496505

497506
flusher, ok := fc.responseWriter.(http.Flusher)
498507
if !ok {
499-
return
508+
return true
509+
}
510+
511+
if clientHasClosed(r) {
512+
return true
500513
}
501514

502515
flusher.Flush()
516+
517+
return false
503518
}
504519

505520
//export go_read_post

frankenphp_test.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package frankenphp_test
22

33
import (
4+
"context"
45
"fmt"
56
"io"
67
"log"
@@ -15,6 +16,7 @@ import (
1516
"strings"
1617
"sync"
1718
"testing"
19+
"time"
1820

1921
"github.com/dunglas/frankenphp"
2022
"github.com/stretchr/testify/assert"
@@ -388,6 +390,102 @@ func testLog(t *testing.T, opts *testOptions) {
388390
}, opts)
389391
}
390392

393+
func TestConnectionAbortNormal_module(t *testing.T) { testConnectionAbortNormal(t, &testOptions{}) }
394+
func TestConnectionAbortNormal_worker(t *testing.T) {
395+
testConnectionAbortNormal(t, &testOptions{workerScript: "connectionStatusLog.php"})
396+
}
397+
func testConnectionAbortNormal(t *testing.T, opts *testOptions) {
398+
logger, logs := observer.New(zap.InfoLevel)
399+
opts.logger = zap.New(logger)
400+
401+
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
402+
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/connectionStatusLog.php?i=%d", i), nil)
403+
w := httptest.NewRecorder()
404+
405+
ctx, cancel := context.WithCancel(req.Context())
406+
req = req.WithContext(ctx)
407+
cancel()
408+
handler(w, req)
409+
410+
// todo: remove conditions on wall clock to avoid race conditions/flakiness
411+
time.Sleep(1000 * time.Microsecond)
412+
var found bool
413+
searched := fmt.Sprintf("request %d: 1", i)
414+
for _, entry := range logs.All() {
415+
if entry.Message == searched {
416+
found = true
417+
break
418+
}
419+
}
420+
421+
assert.True(t, found)
422+
}, opts)
423+
}
424+
425+
func TestConnectionAbortFlush_module(t *testing.T) { testConnectionAbortFlush(t, &testOptions{}) }
426+
func TestConnectionAbortFlush_worker(t *testing.T) {
427+
testConnectionAbortFlush(t, &testOptions{workerScript: "connectionStatusLog.php"})
428+
}
429+
func testConnectionAbortFlush(t *testing.T, opts *testOptions) {
430+
logger, logs := observer.New(zap.InfoLevel)
431+
opts.logger = zap.New(logger)
432+
433+
runTest(t, func(handler func(w http.ResponseWriter, response *http.Request), _ *httptest.Server, i int) {
434+
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/connectionStatusLog.php?i=%d&flush", i), nil)
435+
w := httptest.NewRecorder()
436+
437+
ctx, cancel := context.WithCancel(req.Context())
438+
req = req.WithContext(ctx)
439+
cancel()
440+
handler(w, req)
441+
442+
// todo: remove conditions on wall clock to avoid race conditions/flakiness
443+
time.Sleep(1000 * time.Microsecond)
444+
var found bool
445+
searched := fmt.Sprintf("request %d: 1", i)
446+
for _, entry := range logs.All() {
447+
if entry.Message == searched {
448+
found = true
449+
break
450+
}
451+
}
452+
453+
assert.True(t, found)
454+
}, opts)
455+
}
456+
457+
func TestConnectionAbortFinish_module(t *testing.T) { testConnectionAbortFinish(t, &testOptions{}) }
458+
func TestConnectionAbortFinish_worker(t *testing.T) {
459+
testConnectionAbortFinish(t, &testOptions{workerScript: "connectionStatusLog.php"})
460+
}
461+
func testConnectionAbortFinish(t *testing.T, opts *testOptions) {
462+
logger, logs := observer.New(zap.InfoLevel)
463+
opts.logger = zap.New(logger)
464+
465+
runTest(t, func(handler func(w http.ResponseWriter, response *http.Request), _ *httptest.Server, i int) {
466+
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/connectionStatusLog.php?i=%d&finish", i), nil)
467+
w := httptest.NewRecorder()
468+
469+
ctx, cancel := context.WithCancel(req.Context())
470+
req = req.WithContext(ctx)
471+
cancel()
472+
handler(w, req)
473+
474+
// todo: remove conditions on wall clock to avoid race conditions/flakiness
475+
time.Sleep(1000 * time.Microsecond)
476+
var found bool
477+
searched := fmt.Sprintf("request %d: 0", i)
478+
for _, entry := range logs.All() {
479+
if entry.Message == searched {
480+
found = true
481+
break
482+
}
483+
}
484+
485+
assert.True(t, found)
486+
}, opts)
487+
}
488+
391489
func TestException_module(t *testing.T) { testException(t, &testOptions{}) }
392490
func TestException_worker(t *testing.T) {
393491
testException(t, &testOptions{workerScript: "exception.php"})

testdata/connectionStatusLog.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
ignore_user_abort(true);
4+
5+
require_once __DIR__.'/_executor.php';
6+
7+
return function () {
8+
if(isset($_GET['finish'])) {
9+
frankenphp_finish_request();
10+
}
11+
echo 'hi';
12+
if(isset($_GET['flush'])) {
13+
flush();
14+
}
15+
$status = (string) connection_status();
16+
error_log("request {$_GET['i']}: " . $status);
17+
};

0 commit comments

Comments
 (0)