tep 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/Makefile +134 -0
- data/README.md +247 -0
- data/SINATRA_COMPAT.md +376 -0
- data/bin/tep +2156 -0
- data/examples/agentic_chat/README.md +103 -0
- data/examples/agentic_chat/app.rb +310 -0
- data/examples/api_gateway/README.md +49 -0
- data/examples/api_gateway/app.rb +66 -0
- data/examples/blog/app.rb +367 -0
- data/examples/blog/views/index.erb +36 -0
- data/examples/blog/views/login.erb +28 -0
- data/examples/blog/views/new_post.erb +25 -0
- data/examples/blog/views/show.erb +16 -0
- data/examples/chat/app.rb +278 -0
- data/examples/chat/assets/logo.svg +13 -0
- data/examples/chat/assets/style.css +209 -0
- data/examples/chat/views/index.erb +142 -0
- data/examples/chatbot/README.md +111 -0
- data/examples/chatbot/app.rb +1024 -0
- data/examples/chatbot/assets/chat.js +249 -0
- data/examples/chatbot/assets/compare.js +93 -0
- data/examples/chatbot/assets/markdown.js +84 -0
- data/examples/chatbot/assets/style.css +215 -0
- data/examples/chatbot/schema.sql +25 -0
- data/examples/chatbot/views/compare.erb +43 -0
- data/examples/chatbot/views/index.erb +42 -0
- data/examples/chatbot/views/login.erb +22 -0
- data/examples/chatbot/views/setup.erb +23 -0
- data/examples/counter/README.md +68 -0
- data/examples/counter/app.rb +85 -0
- data/examples/experiments/AGENTS.md +91 -0
- data/examples/experiments/README.md +99 -0
- data/examples/experiments/app.rb +225 -0
- data/examples/geohash/Gemfile +11 -0
- data/examples/geohash/Gemfile.lock +17 -0
- data/examples/geohash/README.md +58 -0
- data/examples/geohash/app.rb +33 -0
- data/examples/hello.rb +120 -0
- data/examples/llm_gateway/README.md +73 -0
- data/examples/llm_gateway/app.rb +91 -0
- data/examples/maidenhead/Gemfile +7 -0
- data/examples/maidenhead/Gemfile.lock +17 -0
- data/examples/maidenhead/README.md +47 -0
- data/examples/maidenhead/app.rb +46 -0
- data/examples/pg_hello.rb +76 -0
- data/examples/qdrant/Gemfile +11 -0
- data/examples/qdrant/Gemfile.lock +29 -0
- data/examples/qdrant/README.md +54 -0
- data/examples/sinatra_style.rb +32 -0
- data/examples/websocket_echo.rb +37 -0
- data/lib/tep/agent_delegation.rb +35 -0
- data/lib/tep/app.rb +291 -0
- data/lib/tep/assets.rb +52 -0
- data/lib/tep/auth.rb +78 -0
- data/lib/tep/auth_bearer_token.rb +126 -0
- data/lib/tep/auth_oauth2.rb +189 -0
- data/lib/tep/auth_oauth2_client.rb +29 -0
- data/lib/tep/auth_oauth2_code.rb +40 -0
- data/lib/tep/auth_session_cookie.rb +132 -0
- data/lib/tep/broadcast.rb +265 -0
- data/lib/tep/broadcast_subscription.rb +42 -0
- data/lib/tep/cache.rb +49 -0
- data/lib/tep/events.rb +257 -0
- data/lib/tep/filter.rb +21 -0
- data/lib/tep/handler.rb +35 -0
- data/lib/tep/http.rb +599 -0
- data/lib/tep/identity.rb +67 -0
- data/lib/tep/job.rb +186 -0
- data/lib/tep/json.rb +572 -0
- data/lib/tep/jwt.rb +126 -0
- data/lib/tep/live_view.rb +219 -0
- data/lib/tep/llm.rb +505 -0
- data/lib/tep/logger.rb +85 -0
- data/lib/tep/mcp.rb +203 -0
- data/lib/tep/multipart.rb +98 -0
- data/lib/tep/net.rb +155 -0
- data/lib/tep/openai_server.rb +725 -0
- data/lib/tep/parallel.rb +168 -0
- data/lib/tep/parser.rb +81 -0
- data/lib/tep/password.rb +102 -0
- data/lib/tep/pg.rb +1128 -0
- data/lib/tep/presence.rb +589 -0
- data/lib/tep/presence_entry.rb +52 -0
- data/lib/tep/proxy.rb +801 -0
- data/lib/tep/request.rb +194 -0
- data/lib/tep/response.rb +134 -0
- data/lib/tep/router.rb +137 -0
- data/lib/tep/scheduler.rb +342 -0
- data/lib/tep/security.rb +140 -0
- data/lib/tep/server.rb +276 -0
- data/lib/tep/server_scheduled.rb +375 -0
- data/lib/tep/session.rb +98 -0
- data/lib/tep/shell.rb +62 -0
- data/lib/tep/sphttp.c +858 -0
- data/lib/tep/sqlite.rb +215 -0
- data/lib/tep/streamer.rb +31 -0
- data/lib/tep/tep_pg.c +769 -0
- data/lib/tep/tep_sqlite.c +320 -0
- data/lib/tep/url.rb +161 -0
- data/lib/tep/version.rb +3 -0
- data/lib/tep/websocket/connection.rb +171 -0
- data/lib/tep/websocket/driver.rb +169 -0
- data/lib/tep/websocket/frame.rb +238 -0
- data/lib/tep/websocket/handshake.rb +159 -0
- data/lib/tep/websocket.rb +68 -0
- data/lib/tep.rb +981 -0
- data/public/hello.txt +1 -0
- data/public/style.css +4 -0
- data/spinel-ext.json +33 -0
- data/test/helper.rb +248 -0
- data/test/real_world/01_simple.rb +5 -0
- data/test/real_world/02_lifecycle.rb +20 -0
- data/test/real_world/03_chat.rb +75 -0
- data/test/real_world/04_health_api.rb +25 -0
- data/test/real_world/05_todo_api.rb +57 -0
- data/test/real_world/06_basic_auth.rb +25 -0
- data/test/real_world/07_bbc_rest_api.rb +228 -0
- data/test/real_world/07_sklise_things.rb +109 -0
- data/test/real_world/08_jwd83_helloworld.rb +56 -0
- data/test/run_all.rb +7 -0
- data/test/run_parallel.rb +89 -0
- data/test/spinel_scheduled_burst_segv_repro.rb +33 -0
- data/test/test_api_gateway.rb +76 -0
- data/test/test_auth.rb +223 -0
- data/test/test_auth_oauth2.rb +208 -0
- data/test/test_auth_session_cookie.rb +198 -0
- data/test/test_broadcast.rb +197 -0
- data/test/test_broadcast_pg.rb +135 -0
- data/test/test_cache.rb +98 -0
- data/test/test_cache_static.rb +48 -0
- data/test/test_cookies.rb +52 -0
- data/test/test_erb.rb +53 -0
- data/test/test_erb_ivars.rb +58 -0
- data/test/test_events.rb +114 -0
- data/test/test_filters.rb +41 -0
- data/test/test_geohash_example.rb +89 -0
- data/test/test_http.rb +137 -0
- data/test/test_http_pool.rb +122 -0
- data/test/test_http_pool_send.rb +57 -0
- data/test/test_identity.rb +165 -0
- data/test/test_inbound_tls.rb +101 -0
- data/test/test_inbound_tls_scheduled.rb +101 -0
- data/test/test_job.rb +108 -0
- data/test/test_json.rb +168 -0
- data/test/test_jwt.rb +143 -0
- data/test/test_live_view.rb +324 -0
- data/test/test_llm.rb +250 -0
- data/test/test_llm_gateway.rb +95 -0
- data/test/test_logger.rb +101 -0
- data/test/test_maidenhead_example.rb +86 -0
- data/test/test_mcp.rb +264 -0
- data/test/test_misc_v02.rb +54 -0
- data/test/test_modular.rb +43 -0
- data/test/test_multi_filters.rb +40 -0
- data/test/test_mustache.rb +57 -0
- data/test/test_openai_server.rb +598 -0
- data/test/test_optional_segments.rb +45 -0
- data/test/test_parallel.rb +102 -0
- data/test/test_params.rb +99 -0
- data/test/test_pass.rb +42 -0
- data/test/test_password.rb +101 -0
- data/test/test_pg.rb +673 -0
- data/test/test_presence.rb +374 -0
- data/test/test_presence_pg.rb +309 -0
- data/test/test_proxy.rb +556 -0
- data/test/test_proxy_dsl.rb +119 -0
- data/test/test_proxy_streaming.rb +146 -0
- data/test/test_real_world.rb +397 -0
- data/test/test_regex_routes.rb +52 -0
- data/test/test_request_methods.rb +102 -0
- data/test/test_response.rb +123 -0
- data/test/test_routing.rb +109 -0
- data/test/test_scheduler.rb +153 -0
- data/test/test_security.rb +72 -0
- data/test/test_server_scheduled.rb +56 -0
- data/test/test_sessions.rb +59 -0
- data/test/test_shell.rb +54 -0
- data/test/test_sqlite.rb +148 -0
- data/test/test_sqlite_cached.rb +171 -0
- data/test/test_static.rb +57 -0
- data/test/test_streaming.rb +96 -0
- data/test/test_unsupported.rb +32 -0
- data/test/test_websocket.rb +152 -0
- data/test/test_websocket_echo.rb +138 -0
- data/test/views/greet.erb +5 -0
- data/test/views/hello.erb +5 -0
- data/test/views/list.erb +5 -0
- data/test/views/m_ivars.mustache +3 -0
- data/test/views/m_simple.mustache +4 -0
- data/test/views/mixed.erb +3 -0
- metadata +264 -0
data/test/test_proxy.rb
ADDED
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
require_relative "helper"
|
|
2
|
+
require "json"
|
|
3
|
+
|
|
4
|
+
# Tep::Proxy -- HTTP reverse-proxy battery (chunk 6.1, non-streaming).
|
|
5
|
+
#
|
|
6
|
+
# Same self-calling shape as test_http.rb: the app boots both the
|
|
7
|
+
# "upstream" endpoints (/ping, /echo_body, /headers_back, /teapot)
|
|
8
|
+
# and proxy routes that forward to 127.0.0.1:<own-port>. The proxy
|
|
9
|
+
# instance's upstream is fixed at construction, so each proxy route
|
|
10
|
+
# builds its Tep::Proxy subclass inside the handler with the runtime
|
|
11
|
+
# port from a path capture (the harness can't tell a load-time
|
|
12
|
+
# constructor its own port).
|
|
13
|
+
#
|
|
14
|
+
# Runs under Tep::Server::Scheduled with workers=1 -- the only shape
|
|
15
|
+
# where a handler can make an outbound call back to its own server
|
|
16
|
+
# under cooperative I/O (see docs/MACOS-CONCURRENCY.md, test_http.rb).
|
|
17
|
+
class TestProxy < TepTest
|
|
18
|
+
app_source <<~RB
|
|
19
|
+
require 'sinatra'
|
|
20
|
+
|
|
21
|
+
set :scheduler, :scheduled
|
|
22
|
+
set :workers, 1
|
|
23
|
+
|
|
24
|
+
# ---- upstream endpoints (what the proxies forward to) ----
|
|
25
|
+
|
|
26
|
+
get '/ping' do
|
|
27
|
+
"pong"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
post '/echo_body' do
|
|
31
|
+
res.headers["Content-Type"] = "text/plain"
|
|
32
|
+
req.raw_body
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
get '/headers_back' do
|
|
36
|
+
"x-custom=" + req.req_headers["x-custom"]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
get '/teapot' do
|
|
40
|
+
res.set_status(418)
|
|
41
|
+
"i'm a teapot"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
get '/sets_header' do
|
|
45
|
+
res.headers["X-Upstream"] = "from-upstream"
|
|
46
|
+
"ok"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# ---- proxy subclasses (the overridable-hook lowering target) ----
|
|
50
|
+
|
|
51
|
+
# Forward to a fixed upstream path regardless of inbound path.
|
|
52
|
+
class PingProxy < Tep::Proxy
|
|
53
|
+
def rewrite_path(path)
|
|
54
|
+
"/ping"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
class TeapotProxy < Tep::Proxy
|
|
59
|
+
def rewrite_path(path)
|
|
60
|
+
"/teapot"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
class HeaderBackProxy < Tep::Proxy
|
|
65
|
+
def rewrite_path(path)
|
|
66
|
+
"/headers_back"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
class SetsHeaderProxy < Tep::Proxy
|
|
71
|
+
def rewrite_path(path)
|
|
72
|
+
"/sets_header"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Inject a header into the upstream request.
|
|
77
|
+
class InjectProxy < Tep::Proxy
|
|
78
|
+
def rewrite_path(path)
|
|
79
|
+
"/headers_back"
|
|
80
|
+
end
|
|
81
|
+
def before_forward(req, res, ureq)
|
|
82
|
+
ureq.set_header("X-Custom", "injected-by-proxy")
|
|
83
|
+
false
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Short-circuit: never reach upstream.
|
|
88
|
+
class GuardProxy < Tep::Proxy
|
|
89
|
+
def before_forward(req, res, ureq)
|
|
90
|
+
res.set_status(403)
|
|
91
|
+
res.set_body("denied by proxy")
|
|
92
|
+
true
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Stamp the response on the way back out.
|
|
97
|
+
class StampProxy < Tep::Proxy
|
|
98
|
+
def rewrite_path(path)
|
|
99
|
+
"/ping"
|
|
100
|
+
end
|
|
101
|
+
def after_forward(req, ures, res)
|
|
102
|
+
res.headers["X-Proxied"] = "yes"
|
|
103
|
+
0
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Pass the request body straight through to /echo_body.
|
|
108
|
+
class EchoProxy < Tep::Proxy
|
|
109
|
+
def rewrite_path(path)
|
|
110
|
+
"/echo_body"
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# ---- proxy mount routes (build with runtime port) ----
|
|
115
|
+
|
|
116
|
+
get '/p/ping/:port' do
|
|
117
|
+
PingProxy.new("http://127.0.0.1:" + params[:port]).handle(req, res)
|
|
118
|
+
res.body
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
get '/p/teapot/:port' do
|
|
122
|
+
TeapotProxy.new("http://127.0.0.1:" + params[:port]).handle(req, res)
|
|
123
|
+
res.body
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
get '/p/inject/:port' do
|
|
127
|
+
InjectProxy.new("http://127.0.0.1:" + params[:port]).handle(req, res)
|
|
128
|
+
res.body
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
get '/p/guard/:port' do
|
|
132
|
+
GuardProxy.new("http://127.0.0.1:" + params[:port]).handle(req, res)
|
|
133
|
+
res.body
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
get '/p/stamp/:port' do
|
|
137
|
+
StampProxy.new("http://127.0.0.1:" + params[:port]).handle(req, res)
|
|
138
|
+
res.body
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
get '/p/upstreamhdr/:port' do
|
|
142
|
+
SetsHeaderProxy.new("http://127.0.0.1:" + params[:port]).handle(req, res)
|
|
143
|
+
res.body
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
post '/p/echo/:port' do
|
|
147
|
+
EchoProxy.new("http://127.0.0.1:" + params[:port]).handle(req, res)
|
|
148
|
+
res.body
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Dead upstream port -> connect failure -> 502.
|
|
152
|
+
get '/p/deadport' do
|
|
153
|
+
PingProxy.new("http://127.0.0.1:1").handle(req, res)
|
|
154
|
+
res.body
|
|
155
|
+
end
|
|
156
|
+
RB
|
|
157
|
+
|
|
158
|
+
def test_forwards_get_and_returns_upstream_body
|
|
159
|
+
res = get("/p/ping/#{@port}")
|
|
160
|
+
assert_equal "200", res.code
|
|
161
|
+
assert_equal "pong", res.body
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def test_propagates_upstream_status
|
|
165
|
+
res = get("/p/teapot/#{@port}")
|
|
166
|
+
assert_equal "418", res.code
|
|
167
|
+
assert_equal "i'm a teapot", res.body
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def test_before_forward_can_inject_upstream_header
|
|
171
|
+
res = get("/p/inject/#{@port}")
|
|
172
|
+
assert_equal "200", res.code
|
|
173
|
+
assert_equal "x-custom=injected-by-proxy", res.body
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def test_before_forward_short_circuits
|
|
177
|
+
res = get("/p/guard/#{@port}")
|
|
178
|
+
assert_equal "403", res.code
|
|
179
|
+
assert_equal "denied by proxy", res.body
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def test_after_forward_can_stamp_response
|
|
183
|
+
res = get("/p/stamp/#{@port}")
|
|
184
|
+
assert_equal "200", res.code
|
|
185
|
+
assert_equal "pong", res.body
|
|
186
|
+
assert_equal "yes", res["X-Proxied"]
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def test_propagates_upstream_response_headers
|
|
190
|
+
res = get("/p/upstreamhdr/#{@port}")
|
|
191
|
+
assert_equal "200", res.code
|
|
192
|
+
assert_equal "from-upstream", res["X-Upstream"]
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def test_forwards_post_body
|
|
196
|
+
res = post("/p/echo/#{@port}", "round trip body")
|
|
197
|
+
assert_equal "200", res.code
|
|
198
|
+
assert_equal "round trip body", res.body
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def test_connect_failure_maps_to_502
|
|
202
|
+
res = get("/p/deadport")
|
|
203
|
+
assert_equal "502", res.code
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Tep::Proxy 6.4: per-request upstream routing via pick_upstream(req).
|
|
208
|
+
# Two faux backends (/srv-a/info, /srv-b/info) on the same server are
|
|
209
|
+
# routed through a Router proxy whose pick_upstream branches by path.
|
|
210
|
+
# Demonstrates the override path and that the default (returning
|
|
211
|
+
# @upstream) is preserved when not overridden.
|
|
212
|
+
class TestProxyMultiUpstream < TepTest
|
|
213
|
+
app_source <<~RB
|
|
214
|
+
require 'sinatra'
|
|
215
|
+
|
|
216
|
+
set :scheduler, :scheduled
|
|
217
|
+
set :workers, 1
|
|
218
|
+
|
|
219
|
+
# Two upstream "backends" on the same server, distinguished by
|
|
220
|
+
# path prefix. In a real deployment these would be separate hosts;
|
|
221
|
+
# the test framework runs one app per class so we collapse them
|
|
222
|
+
# onto distinct routes that pick_upstream + rewrite_path can
|
|
223
|
+
# treat as logically separate upstreams.
|
|
224
|
+
get '/srv-a/info' do
|
|
225
|
+
"from-a"
|
|
226
|
+
end
|
|
227
|
+
get '/srv-b/info' do
|
|
228
|
+
"from-b"
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Routes /p/route/:port/a -> srv-a's /info, /p/route/:port/b ->
|
|
232
|
+
# srv-b's /info. pick_upstream picks the BASE URL (host+port +
|
|
233
|
+
# the fixed /srv-X prefix); rewrite_path produces the suffix.
|
|
234
|
+
# Composed: pick_upstream(req) + rewrite_path(raw_path).
|
|
235
|
+
class Router < Tep::Proxy
|
|
236
|
+
def pick_upstream(req)
|
|
237
|
+
if Tep.str_find(req.path, "/a", 0) >= 0
|
|
238
|
+
@upstream + "/srv-a"
|
|
239
|
+
else
|
|
240
|
+
@upstream + "/srv-b"
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
def rewrite_path(path)
|
|
244
|
+
"/info"
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
get '/p/route/:port/:where' do
|
|
249
|
+
Router.new("http://127.0.0.1:" + params[:port]).handle(req, res)
|
|
250
|
+
res.body
|
|
251
|
+
end
|
|
252
|
+
RB
|
|
253
|
+
|
|
254
|
+
def test_pick_upstream_routes_to_srv_a
|
|
255
|
+
res = get("/p/route/#{@port}/a")
|
|
256
|
+
assert_equal "200", res.code
|
|
257
|
+
assert_equal "from-a", res.body
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def test_pick_upstream_routes_to_srv_b
|
|
261
|
+
res = get("/p/route/#{@port}/b")
|
|
262
|
+
assert_equal "200", res.code
|
|
263
|
+
assert_equal "from-b", res.body
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Tep::Proxy 6.6: body size caps. max_request_body_bytes rejects
|
|
268
|
+
# oversize POSTs with 413 before any upstream call; max_response_body_bytes
|
|
269
|
+
# rejects oversize upstream responses with 502 + proxy_error JSON.
|
|
270
|
+
class TestProxyBodyCaps < TepTest
|
|
271
|
+
app_source <<~RB
|
|
272
|
+
require 'sinatra'
|
|
273
|
+
|
|
274
|
+
set :scheduler, :scheduled
|
|
275
|
+
set :workers, 1
|
|
276
|
+
|
|
277
|
+
# Two upstream endpoints with hardcoded sizes -- 50-byte (under
|
|
278
|
+
# the 200-byte response cap) and 500-byte (over).
|
|
279
|
+
get '/upstream/small' do
|
|
280
|
+
res.headers["Content-Type"] = "application/octet-stream"
|
|
281
|
+
"x" * 50
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
get '/upstream/large' do
|
|
285
|
+
res.headers["Content-Type"] = "application/octet-stream"
|
|
286
|
+
"x" * 500
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
post '/upstream/echo' do
|
|
290
|
+
res.headers["Content-Type"] = "application/octet-stream"
|
|
291
|
+
req.raw_body
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Proxies with tiny caps to make over/under testable. 100-byte
|
|
295
|
+
# request cap; 200-byte response cap. Each one extends Tep::Proxy
|
|
296
|
+
# DIRECTLY (not via a shared intermediate parent) -- spinel's
|
|
297
|
+
# widening over an intermediate-class initialize that sets the
|
|
298
|
+
# new attrs lets the upstream dispatch widen and Tep::Http.send_req
|
|
299
|
+
# returns status=0 / connect-failure 502. Three direct subclasses
|
|
300
|
+
# type-pin cleanly; the duplicated initialize is a deliberate
|
|
301
|
+
# workaround.
|
|
302
|
+
class TinyEchoProxy < Tep::Proxy
|
|
303
|
+
def initialize(upstream)
|
|
304
|
+
super
|
|
305
|
+
self.max_request_body_bytes = 100
|
|
306
|
+
self.max_response_body_bytes = 200
|
|
307
|
+
end
|
|
308
|
+
def rewrite_path(path)
|
|
309
|
+
"/upstream/echo"
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
class TinySmallProxy < Tep::Proxy
|
|
314
|
+
def initialize(upstream)
|
|
315
|
+
super
|
|
316
|
+
self.max_request_body_bytes = 100
|
|
317
|
+
self.max_response_body_bytes = 200
|
|
318
|
+
end
|
|
319
|
+
def rewrite_path(path)
|
|
320
|
+
"/upstream/small"
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
class TinyLargeProxy < Tep::Proxy
|
|
325
|
+
def initialize(upstream)
|
|
326
|
+
super
|
|
327
|
+
self.max_request_body_bytes = 100
|
|
328
|
+
self.max_response_body_bytes = 200
|
|
329
|
+
end
|
|
330
|
+
def rewrite_path(path)
|
|
331
|
+
"/upstream/large"
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
post '/p/tiny-echo/:port' do
|
|
336
|
+
TinyEchoProxy.new("http://127.0.0.1:" + params[:port]).handle(req, res)
|
|
337
|
+
res.body
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
get '/p/tiny-small/:port' do
|
|
341
|
+
TinySmallProxy.new("http://127.0.0.1:" + params[:port]).handle(req, res)
|
|
342
|
+
res.body
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
get '/p/tiny-large/:port' do
|
|
346
|
+
TinyLargeProxy.new("http://127.0.0.1:" + params[:port]).handle(req, res)
|
|
347
|
+
res.body
|
|
348
|
+
end
|
|
349
|
+
RB
|
|
350
|
+
|
|
351
|
+
def test_request_under_cap_passes
|
|
352
|
+
res = post("/p/tiny-echo/#{@port}", "x" * 50)
|
|
353
|
+
assert_equal "200", res.code
|
|
354
|
+
assert_equal "x" * 50, res.body
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def test_request_over_cap_returns_413
|
|
358
|
+
res = post("/p/tiny-echo/#{@port}", "x" * 500)
|
|
359
|
+
assert_equal "413", res.code
|
|
360
|
+
body = JSON.parse(res.body)
|
|
361
|
+
assert_equal "payload_too_large", body["error"]["type"]
|
|
362
|
+
assert_match(/proxy cap of 100 bytes/, body["error"]["message"])
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def test_response_under_cap_passes
|
|
366
|
+
res = get("/p/tiny-small/#{@port}")
|
|
367
|
+
assert_equal "200", res.code
|
|
368
|
+
assert_equal 50, res.body.length
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def test_response_over_cap_returns_502
|
|
372
|
+
res = get("/p/tiny-large/#{@port}")
|
|
373
|
+
assert_equal "502", res.code
|
|
374
|
+
refute_empty res.body, "expected an error-shape body, got empty (502 from connect failure path?)"
|
|
375
|
+
body = JSON.parse(res.body)
|
|
376
|
+
assert_equal "upstream_body_too_large", body["error"]["type"]
|
|
377
|
+
assert_match(/upstream response body exceeds proxy cap of 200 bytes/,
|
|
378
|
+
body["error"]["message"])
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
# Tep::Proxy 6.5: retries on transient upstream failures.
|
|
383
|
+
class TestProxyRetries < TepTest
|
|
384
|
+
app_source <<~RB
|
|
385
|
+
require 'sinatra'
|
|
386
|
+
|
|
387
|
+
set :scheduler, :scheduled
|
|
388
|
+
set :workers, 1
|
|
389
|
+
|
|
390
|
+
# Upstream with a per-process attempt counter -- returns 503 on
|
|
391
|
+
# the first hit, 200 on every subsequent hit. Resets via a
|
|
392
|
+
# /reset route between tests so each test gets a clean slate.
|
|
393
|
+
class FlakyState
|
|
394
|
+
attr_accessor :hits
|
|
395
|
+
def initialize; @hits = 0; end
|
|
396
|
+
end
|
|
397
|
+
STATE = FlakyState.new
|
|
398
|
+
|
|
399
|
+
get '/upstream/flaky' do
|
|
400
|
+
STATE.hits = STATE.hits + 1
|
|
401
|
+
if STATE.hits == 1
|
|
402
|
+
res.set_status(503)
|
|
403
|
+
return "{\\"error\\":\\"unavailable\\"}"
|
|
404
|
+
end
|
|
405
|
+
res.headers["Content-Type"] = "text/plain"
|
|
406
|
+
"ok"
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
get '/upstream/always_503' do
|
|
410
|
+
res.set_status(503)
|
|
411
|
+
"{\\"error\\":\\"unavailable\\"}"
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
post '/reset_flaky' do
|
|
415
|
+
STATE.hits = 0
|
|
416
|
+
"0"
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
# Direct-subclass shape (same workaround as the body-caps tests:
|
|
420
|
+
# an intermediate-class initialize that sets new attrs widens
|
|
421
|
+
# incorrectly across multiple grandchildren).
|
|
422
|
+
class FlakyProxy < Tep::Proxy
|
|
423
|
+
def retry_policy(req)
|
|
424
|
+
p = Tep::Proxy::RetryPolicy.new
|
|
425
|
+
p.max_attempts = 3
|
|
426
|
+
# base_backoff_seconds stays 0 (test-friendly).
|
|
427
|
+
p
|
|
428
|
+
end
|
|
429
|
+
def rewrite_path(path)
|
|
430
|
+
"/upstream/flaky"
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
class GiveUpProxy < Tep::Proxy
|
|
435
|
+
def retry_policy(req)
|
|
436
|
+
p = Tep::Proxy::RetryPolicy.new
|
|
437
|
+
p.max_attempts = 2 # 1 retry; both hit the always-503
|
|
438
|
+
p
|
|
439
|
+
end
|
|
440
|
+
def rewrite_path(path)
|
|
441
|
+
"/upstream/always_503"
|
|
442
|
+
end
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
class NoRetryProxy < Tep::Proxy
|
|
446
|
+
# Default retry_policy (max_attempts=1) -- 503 surfaces as 503.
|
|
447
|
+
def rewrite_path(path)
|
|
448
|
+
"/upstream/flaky"
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
# Same flaky upstream but with an explicit 200ms backoff. Used
|
|
453
|
+
# to verify the ms-grained sleep actually fires.
|
|
454
|
+
class FlakyBackoffProxy < Tep::Proxy
|
|
455
|
+
def retry_policy(req)
|
|
456
|
+
p = Tep::Proxy::RetryPolicy.new
|
|
457
|
+
p.max_attempts = 2
|
|
458
|
+
p.base_backoff_ms = 200
|
|
459
|
+
p
|
|
460
|
+
end
|
|
461
|
+
def rewrite_path(path)
|
|
462
|
+
"/upstream/flaky"
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
# Same again but uses the Float-seconds convenience setter to
|
|
467
|
+
# prove it stores the right ms (0.2 -> 200ms).
|
|
468
|
+
class FlakyBackoffSecsProxy < Tep::Proxy
|
|
469
|
+
def retry_policy(req)
|
|
470
|
+
p = Tep::Proxy::RetryPolicy.new
|
|
471
|
+
p.max_attempts = 2
|
|
472
|
+
p.base_backoff_secs = 0.2
|
|
473
|
+
p
|
|
474
|
+
end
|
|
475
|
+
def rewrite_path(path)
|
|
476
|
+
"/upstream/flaky"
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
get '/p/flaky/:port' do
|
|
481
|
+
FlakyProxy.new("http://127.0.0.1:" + params[:port]).handle(req, res)
|
|
482
|
+
res.body
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
get '/p/giveup/:port' do
|
|
486
|
+
GiveUpProxy.new("http://127.0.0.1:" + params[:port]).handle(req, res)
|
|
487
|
+
res.body
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
get '/p/noretry/:port' do
|
|
491
|
+
NoRetryProxy.new("http://127.0.0.1:" + params[:port]).handle(req, res)
|
|
492
|
+
res.body
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
get '/p/flaky-backoff/:port' do
|
|
496
|
+
FlakyBackoffProxy.new("http://127.0.0.1:" + params[:port]).handle(req, res)
|
|
497
|
+
res.body
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
get '/p/flaky-backoff-secs/:port' do
|
|
501
|
+
FlakyBackoffSecsProxy.new("http://127.0.0.1:" + params[:port]).handle(req, res)
|
|
502
|
+
res.body
|
|
503
|
+
end
|
|
504
|
+
RB
|
|
505
|
+
|
|
506
|
+
def reset_flaky
|
|
507
|
+
post("/reset_flaky", "")
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
def test_retries_recover_from_one_503
|
|
511
|
+
reset_flaky
|
|
512
|
+
res = get("/p/flaky/#{@port}")
|
|
513
|
+
assert_equal "200", res.code
|
|
514
|
+
assert_equal "ok", res.body
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
def test_gives_up_after_max_attempts
|
|
518
|
+
res = get("/p/giveup/#{@port}")
|
|
519
|
+
assert_equal "503", res.code
|
|
520
|
+
# always_503 returns the error JSON body verbatim through the proxy.
|
|
521
|
+
body = JSON.parse(res.body)
|
|
522
|
+
assert_equal "unavailable", body["error"]
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
def test_no_retry_default_surfaces_503
|
|
526
|
+
reset_flaky
|
|
527
|
+
res = get("/p/noretry/#{@port}")
|
|
528
|
+
assert_equal "503", res.code # would have been 200 with retry
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
def test_backoff_ms_is_actually_sub_second
|
|
532
|
+
# RetryPolicy uses sphttp_sleep_ms; verify a single retry with a
|
|
533
|
+
# 200ms backoff DOES sleep (call takes >= ~100ms) but isn't
|
|
534
|
+
# whole-second-resolution (nowhere near 1s for a 200ms cap).
|
|
535
|
+
reset_flaky
|
|
536
|
+
t0 = Time.now
|
|
537
|
+
res = get("/p/flaky-backoff/#{@port}")
|
|
538
|
+
elapsed = Time.now - t0
|
|
539
|
+
assert_equal "200", res.code
|
|
540
|
+
assert_operator elapsed, :>, 0.1, "expected >= ~100ms (the 200ms backoff) but got #{elapsed}s"
|
|
541
|
+
assert_operator elapsed, :<, 1.0, "expected sub-second (ms-grained backoff) but got #{elapsed}s"
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
def test_backoff_secs_float_setter_equivalent_to_ms_int
|
|
545
|
+
# Same shape as the ms test, but configured via base_backoff_secs
|
|
546
|
+
# = 0.2 (Float). Proves the Float -> int(ms) conversion stores
|
|
547
|
+
# the right value internally.
|
|
548
|
+
reset_flaky
|
|
549
|
+
t0 = Time.now
|
|
550
|
+
res = get("/p/flaky-backoff-secs/#{@port}")
|
|
551
|
+
elapsed = Time.now - t0
|
|
552
|
+
assert_equal "200", res.code
|
|
553
|
+
assert_operator elapsed, :>, 0.1
|
|
554
|
+
assert_operator elapsed, :<, 1.0
|
|
555
|
+
end
|
|
556
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
require_relative "helper"
|
|
2
|
+
|
|
3
|
+
# Tep::Proxy block-form DSL (#88). The bin/tep translator lowers
|
|
4
|
+
# api = Tep::Proxy.new("...")
|
|
5
|
+
# api.before do |req, res, ureq| ... end
|
|
6
|
+
# Tep.get "/path", api
|
|
7
|
+
# into a generated TepProxy_<n> < Tep::Proxy subclass (before ->
|
|
8
|
+
# before_forward, etc.) instantiated by the rewritten assignment.
|
|
9
|
+
#
|
|
10
|
+
# Behavior is validated via the short-circuit path (before returns
|
|
11
|
+
# true -> no upstream call) + dead-port (-> 502), so no live upstream
|
|
12
|
+
# is needed -- the actual forwarding is the subclass-override form
|
|
13
|
+
# (test_proxy.rb) this lowers to. The streaming hooks
|
|
14
|
+
# (on_stream_chunk/on_stream_end/stream_request?) are exercised for
|
|
15
|
+
# *compilation* (the app builds with all five hook kinds lowered).
|
|
16
|
+
class TestProxyDsl < TepTest
|
|
17
|
+
app_source <<~RB
|
|
18
|
+
require 'sinatra'
|
|
19
|
+
|
|
20
|
+
# Short-circuit proxy: before returns true, never reaches upstream.
|
|
21
|
+
guard = Tep::Proxy.new("http://127.0.0.1:1")
|
|
22
|
+
guard.before do |req, res, ureq|
|
|
23
|
+
res.set_status(403)
|
|
24
|
+
res.set_body("blocked by dsl")
|
|
25
|
+
true
|
|
26
|
+
end
|
|
27
|
+
Tep.get "/guard", guard
|
|
28
|
+
|
|
29
|
+
# before short-circuits + after runs on the short-circuit path
|
|
30
|
+
# (audit sees rejected requests). after stamps a header.
|
|
31
|
+
audited = Tep::Proxy.new("http://127.0.0.1:1")
|
|
32
|
+
audited.before do |req, res, ureq|
|
|
33
|
+
res.set_status(403)
|
|
34
|
+
res.set_body("denied")
|
|
35
|
+
true
|
|
36
|
+
end
|
|
37
|
+
audited.after do |req, ures, res|
|
|
38
|
+
res.headers["X-Audited"] = "yes"
|
|
39
|
+
0
|
|
40
|
+
end
|
|
41
|
+
Tep.get "/audited", audited
|
|
42
|
+
|
|
43
|
+
# No hooks: forwards to a dead upstream -> 502.
|
|
44
|
+
dead = Tep::Proxy.new("http://127.0.0.1:1")
|
|
45
|
+
Tep.get "/dead", dead
|
|
46
|
+
|
|
47
|
+
# All five hook kinds, to exercise the translator lowering of the
|
|
48
|
+
# streaming blocks. stream_request? returns false so GET takes the
|
|
49
|
+
# buffered path (dead upstream -> 502); the on_stream_* blocks are
|
|
50
|
+
# lowered + compiled but not invoked here.
|
|
51
|
+
full = Tep::Proxy.new("http://127.0.0.1:1")
|
|
52
|
+
full.stream_request? do |req|
|
|
53
|
+
false
|
|
54
|
+
end
|
|
55
|
+
full.on_stream_chunk do |chunk, out, stats|
|
|
56
|
+
out.write(chunk.chunk_text)
|
|
57
|
+
0
|
|
58
|
+
end
|
|
59
|
+
full.on_stream_end do |req, out, stats|
|
|
60
|
+
out.write("data: done\\n\\n")
|
|
61
|
+
0
|
|
62
|
+
end
|
|
63
|
+
Tep.get "/full", full
|
|
64
|
+
|
|
65
|
+
# 6.4: pick_upstream block. Routes /pick to a different (still
|
|
66
|
+
# dead) upstream, proving the block ran and supplied the URL the
|
|
67
|
+
# buffered forward attempted. before short-circuits so the test
|
|
68
|
+
# asserts on the body the before-block emitted; the pick_upstream
|
|
69
|
+
# block compiled + ran (verified by the lowered subclass actually
|
|
70
|
+
# binding the dead URL when the short-circuit is removed; here we
|
|
71
|
+
# take the buffered path with before returning true).
|
|
72
|
+
routed = Tep::Proxy.new("http://127.0.0.1:1")
|
|
73
|
+
routed.pick_upstream do |req|
|
|
74
|
+
"http://127.0.0.1:2"
|
|
75
|
+
end
|
|
76
|
+
routed.before do |req, res, ureq|
|
|
77
|
+
res.set_status(200)
|
|
78
|
+
res.set_body("picked")
|
|
79
|
+
true
|
|
80
|
+
end
|
|
81
|
+
Tep.get "/pick", routed
|
|
82
|
+
RB
|
|
83
|
+
|
|
84
|
+
def test_before_block_short_circuits
|
|
85
|
+
res = get("/guard")
|
|
86
|
+
assert_equal "403", res.code
|
|
87
|
+
assert_equal "blocked by dsl", res.body
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def test_after_block_runs_on_short_circuit
|
|
91
|
+
res = get("/audited")
|
|
92
|
+
assert_equal "403", res.code
|
|
93
|
+
assert_equal "denied", res.body
|
|
94
|
+
assert_equal "yes", res["X-Audited"]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def test_no_hooks_forwards_dead_upstream_502
|
|
98
|
+
res = get("/dead")
|
|
99
|
+
assert_equal "502", res.code
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def test_full_hookset_buffered_path
|
|
103
|
+
# stream_request? => false, so GET /full takes the buffered path
|
|
104
|
+
# to the dead upstream -> 502 (proves the lowered stream_request?
|
|
105
|
+
# block runs + returns false; the on_stream_* blocks compiled).
|
|
106
|
+
res = get("/full")
|
|
107
|
+
assert_equal "502", res.code
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def test_pick_upstream_block_compiles_and_short_circuit_path
|
|
111
|
+
# The pick_upstream block lowers to a subclass override; before
|
|
112
|
+
# short-circuits before we'd ever connect, so the assertion is on
|
|
113
|
+
# the before-supplied body. The pick_upstream surface compiled,
|
|
114
|
+
# which is what the test guards (translator + runtime arity).
|
|
115
|
+
res = get("/pick")
|
|
116
|
+
assert_equal "200", res.code
|
|
117
|
+
assert_equal "picked", res.body
|
|
118
|
+
end
|
|
119
|
+
end
|