| 1 | import flag |
| 2 | import net.http |
| 3 | import os |
| 4 | import time |
| 5 | |
| 6 | const probe_url = 'https://httpbin.org/post' |
| 7 | const vschannel_16kb_boundary = 16 * 1024 |
| 8 | const default_sizes_csv = '12000,15000,17000,24000,32768' |
| 9 | |
| 10 | fn main() { |
| 11 | if os.args.len >= 2 && os.args[1] == '--worker' { |
| 12 | run_worker_mode() |
| 13 | return |
| 14 | } |
| 15 | run_parent_mode() |
| 16 | } |
| 17 | |
| 18 | fn run_worker_mode() { |
| 19 | if os.args.len < 3 { |
| 20 | eprintln('missing payload size for --worker') |
| 21 | exit(2) |
| 22 | } |
| 23 | size := os.args[2].int() |
| 24 | if size <= 0 { |
| 25 | eprintln('invalid payload size `${os.args[2]}`') |
| 26 | exit(2) |
| 27 | } |
| 28 | payload := '{"size":${size},"payload":"' + 'x'.repeat(size) + '"}' |
| 29 | mut headers := http.new_header() |
| 30 | headers.add(.content_type, 'application/json') |
| 31 | mut req := http.Request{ |
| 32 | method: .post |
| 33 | url: probe_url |
| 34 | header: headers |
| 35 | data: payload |
| 36 | read_timeout: 60 * time.second |
| 37 | write_timeout: 30 * time.second |
| 38 | } |
| 39 | resp := req.do() or { |
| 40 | eprintln('request failed at size=${size}: ${err}') |
| 41 | exit(1) |
| 42 | } |
| 43 | if resp.status_code != 200 { |
| 44 | eprintln('unexpected status at size=${size}: ${resp.status_code}') |
| 45 | exit(1) |
| 46 | } |
| 47 | if !resp.body.contains('httpbin.org/post') { |
| 48 | eprintln('unexpected response body at size=${size}') |
| 49 | exit(1) |
| 50 | } |
| 51 | println('OK size=${size} payload_bytes=${payload.len}') |
| 52 | } |
| 53 | |
| 54 | fn run_parent_mode() { |
| 55 | mut fp := flag.new_flag_parser(os.args) |
| 56 | fp.application('vschannel_16kb_httpbin_probe') |
| 57 | fp.version('0.0.1') |
| 58 | fp.description('Probe net.http HTTPS POST behavior around 16KB payloads using https://httpbin.org/post.') |
| 59 | fp.skip_executable() |
| 60 | |
| 61 | expect_mode := fp.string('expect', `e`, 'after', |
| 62 | 'Expected behavior mode: before|after|none. before expects >16KB failures, after expects all succeed.') |
| 63 | sizes_csv := fp.string('sizes', `s`, default_sizes_csv, |
| 64 | 'Comma separated payload sizes in bytes.') |
| 65 | verbose := fp.bool('verbose', `v`, false, 'Print child process output for all sizes.') |
| 66 | show_help := fp.bool('help', `h`, false, 'Show this help screen.') |
| 67 | free_args := fp.finalize() or { |
| 68 | eprintln('flag parse failed: ${err}') |
| 69 | exit(2) |
| 70 | } |
| 71 | if free_args.len > 0 { |
| 72 | eprintln('unexpected positional args: ${free_args}') |
| 73 | exit(2) |
| 74 | } |
| 75 | if show_help { |
| 76 | println(fp.usage()) |
| 77 | return |
| 78 | } |
| 79 | |
| 80 | mode := expect_mode.to_lower() |
| 81 | if mode !in ['before', 'after', 'none'] { |
| 82 | eprintln('invalid --expect `${expect_mode}` (allowed: before|after|none)') |
| 83 | exit(2) |
| 84 | } |
| 85 | sizes := parse_sizes_csv(sizes_csv) or { |
| 86 | eprintln(err) |
| 87 | exit(2) |
| 88 | } |
| 89 | |
| 90 | self_exe := os.executable() |
| 91 | println('probe url: ${probe_url}') |
| 92 | println('expect mode: ${mode}') |
| 93 | println('sizes: ${sizes}') |
| 94 | |
| 95 | mut mismatches := 0 |
| 96 | for size in sizes { |
| 97 | // Launch worker directly without going through a shell, and capture stdout/stderr separately. |
| 98 | mut p := os.new_process(self_exe) |
| 99 | p.set_args(['--worker', size.str()]) |
| 100 | p.set_redirect_stdio() |
| 101 | p.run() |
| 102 | p.wait() |
| 103 | exit_code := p.code |
| 104 | stdout := p.stdout_read().trim_space() |
| 105 | stderr := p.stderr_read().trim_space() |
| 106 | p.close() |
| 107 | success := exit_code == 0 |
| 108 | expected_success := expected_success_for_mode(mode, size) |
| 109 | status := if success { 'OK' } else { 'FAIL' } |
| 110 | expect_text := if expected_success { 'expect=OK' } else { 'expect=FAIL' } |
| 111 | println('${status:4} size=${size:6} exit=${exit_code:3} ${expect_text}') |
| 112 | if verbose || !success { |
| 113 | if stdout.len > 0 { |
| 114 | println(stdout) |
| 115 | } |
| 116 | if stderr.len > 0 { |
| 117 | eprintln(stderr) |
| 118 | } |
| 119 | } |
| 120 | if mode != 'none' && success != expected_success { |
| 121 | mismatches++ |
| 122 | } |
| 123 | } |
| 124 | |
| 125 | if mismatches > 0 { |
| 126 | eprintln('mismatch count: ${mismatches}') |
| 127 | exit(1) |
| 128 | } |
| 129 | println('probe completed without mismatches') |
| 130 | } |
| 131 | |
| 132 | fn parse_sizes_csv(raw string) ![]int { |
| 133 | mut sizes := []int{} |
| 134 | for part in raw.split(',') { |
| 135 | item := part.trim_space() |
| 136 | if item.len == 0 { |
| 137 | continue |
| 138 | } |
| 139 | size := item.int() |
| 140 | if size <= 0 { |
| 141 | return error('invalid size entry `${item}` in --sizes') |
| 142 | } |
| 143 | sizes << size |
| 144 | } |
| 145 | if sizes.len == 0 { |
| 146 | return error('no sizes provided in --sizes') |
| 147 | } |
| 148 | return sizes |
| 149 | } |
| 150 | |
| 151 | fn expected_success_for_mode(mode string, size int) bool { |
| 152 | return match mode { |
| 153 | 'before' { |
| 154 | size <= vschannel_16kb_boundary |
| 155 | } |
| 156 | 'after' { |
| 157 | true |
| 158 | } |
| 159 | 'none' { |
| 160 | true |
| 161 | } |
| 162 | else { |
| 163 | eprintln('unexpected mode `${mode}` in expected_success_for_mode') |
| 164 | false |
| 165 | } |
| 166 | } |
| 167 | } |
| 168 | |