1- # Hook System
1+ # Action System
22
3- The protocol-proxy provides a flexible hook system that allows applications to inject custom business logic into the routing lifecycle.
3+ The protocol-proxy uses Utopia Platform actions to inject custom business logic into the routing lifecycle.
44
5- ** Key Design** : The proxy doesn't enforce how backends are resolved. Applications provide their own resolution logic via the ` resolve ` hook .
5+ ** Key Design** : The proxy doesn't enforce how backends are resolved. Applications provide their own resolution logic via a ` resolve ` action .
66
7- ## Available Hooks
7+ ## Action Registration
8+
9+ Each adapter initializes a protocol-specific service by default. Use it directly or replace it with your own.
10+
11+ ``` php
12+ use Utopia\Platform\Action;
13+ use Utopia\Proxy\Adapter\HTTP\Swoole as HTTPAdapter;
14+ use Utopia\Proxy\Service\HTTP as HTTPService;
15+
16+ $adapter = new HTTPAdapter();
17+ $service = $adapter->getService() ?? new HTTPService();
18+
19+ // Required: resolve backend endpoint
20+ $service->addAction('resolve', (new class extends Action {})
21+ ->callback(function (string $hostname): string {
22+ return "runtime-{$hostname}.runtimes.svc.cluster.local:8080";
23+ }));
24+
25+ // Optional: beforeRoute actions (TYPE_INIT)
26+ $service->addAction('validateHost', (new class extends Action {})
27+ ->setType(Action::TYPE_INIT)
28+ ->callback(function (string $hostname) {
29+ if (!preg_match('/^[a-z0-9-]+\.myapp\.com$/', $hostname)) {
30+ throw new \Exception("Invalid hostname: {$hostname}");
31+ }
32+ }));
33+
34+ // Optional: afterRoute actions (TYPE_SHUTDOWN)
35+ $service->addAction('logRoute', (new class extends Action {})
36+ ->setType(Action::TYPE_SHUTDOWN)
37+ ->callback(function (string $hostname, string $endpoint, $result) {
38+ error_log("Routed {$hostname} -> {$endpoint}");
39+ }));
40+
41+ // Optional: onRoutingError actions (TYPE_ERROR)
42+ $service->addAction('logError', (new class extends Action {})
43+ ->setType(Action::TYPE_ERROR)
44+ ->callback(function (string $hostname, \Exception $e) {
45+ error_log("Routing error for {$hostname}: {$e->getMessage()}");
46+ }));
47+
48+ $adapter->setService($service);
49+ ```
50+
51+ Actions execute in the order they were added to the service.
52+
53+ ## Protocol Services
54+
55+ Use the protocol-specific service classes to keep configuration aligned with each adapter:
56+
57+ - ` Utopia\Proxy\Service\HTTP `
58+ - ` Utopia\Proxy\Service\TCP `
59+ - ` Utopia\Proxy\Service\SMTP `
60+
61+ ## Action Types and Parameters
862
963### 1. ` resolve ` (Required)
1064
11- Called to ** resolve the backend endpoint ** for a resource identifier.
65+ Action key: ` resolve ` (type is ` Action::TYPE_DEFAULT ` by default)
1266
1367** Parameters:**
1468- ` string $resourceId ` - The identifier to resolve (hostname, domain, etc.)
@@ -24,41 +78,9 @@ Called to **resolve the backend endpoint** for a resource identifier.
2478- Kubernetes service resolution
2579- DNS resolution
2680
27- ** Example:**
28- ``` php
29- // Option 1: Static configuration
30- $adapter->hook('resolve', function (string $hostname) {
31- $mapping = [
32- 'func-123.app.network' => '10.0.1.5:8080',
33- 'func-456.app.network' => '10.0.1.6:8080',
34- ];
35- return $mapping[$hostname] ?? throw new \Exception("Not found");
36- });
37-
38- // Option 2: Database lookup (like Appwrite Edge)
39- $adapter->hook('resolve', function (string $hostname) use ($db) {
40- $doc = $db->findOne('functions', [
41- Query::equal('hostname', [$hostname])
42- ]);
43- return $doc->getAttribute('endpoint');
44- });
45-
46- // Option 3: Service discovery
47- $adapter->hook('resolve', function (string $hostname) use ($consul) {
48- return $consul->resolveService($hostname);
49- });
50-
51- // Option 4: Kubernetes service
52- $adapter->hook('resolve', function (string $hostname) {
53- return "function-{$hostname}.default.svc.cluster.local:8080";
54- });
55- ```
81+ ### 2. ` beforeRoute ` (TYPE_INIT)
5682
57- ** Important:** Only one ` resolve ` hook can be registered. If you try to register multiple, an exception will be thrown.
58-
59- ### 2. ` beforeRoute `
60-
61- Called ** before** any routing logic executes.
83+ Run actions with ` Action::TYPE_INIT ` ** before** routing.
6284
6385** Parameters:**
6486- ` string $resourceId ` - The identifier being routed (hostname, domain, etc.)
@@ -70,24 +92,9 @@ Called **before** any routing logic executes.
7092- Custom caching lookups
7193- Request transformation
7294
73- ** Example:**
74- ``` php
75- $adapter->hook('beforeRoute', function (string $hostname) {
76- // Validate hostname format
77- if (!preg_match('/^[a-z0-9-]+\.myapp\.com$/', $hostname)) {
78- throw new \Exception("Invalid hostname: {$hostname}");
79- }
80-
81- // Check rate limits
82- if (isRateLimited($hostname)) {
83- throw new \Exception("Rate limit exceeded");
84- }
85- });
86- ```
87-
88- ### 2. ` afterRoute `
95+ ### 3. ` afterRoute ` (TYPE_SHUTDOWN)
8996
90- Called ** after** successful routing.
97+ Run actions with ` Action::TYPE_SHUTDOWN ` ** after** successful routing.
9198
9299** Parameters:**
93100- ` string $resourceId ` - The identifier that was routed
@@ -101,28 +108,9 @@ Called **after** successful routing.
101108- Cache warming
102109- Audit trails
103110
104- ** Example:**
105- ``` php
106- $adapter->hook('afterRoute', function (string $hostname, string $endpoint, $result) {
107- // Log to telemetry
108- $telemetry->record([
109- 'hostname' => $hostname,
110- 'endpoint' => $endpoint,
111- 'cached' => $result->metadata['cached'],
112- 'latency_ms' => $result->metadata['latency_ms'],
113- ]);
114-
115- // Update metrics
116- $metrics->increment('proxy.routes.success');
117- if ($result->metadata['cached']) {
118- $metrics->increment('proxy.cache.hits');
119- }
120- });
121- ```
111+ ### 4. ` onRoutingError ` (TYPE_ERROR)
122112
123- ### 3. ` onRoutingError `
124-
125- Called when routing ** fails** with an exception.
113+ Run actions with ` Action::TYPE_ERROR ` when routing fails.
126114
127115** Parameters:**
128116- ` string $resourceId ` - The identifier that failed to route
@@ -135,116 +123,83 @@ Called when routing **fails** with an exception.
135123- Circuit breaker logic
136124- Alerting
137125
138- ** Example:**
139- ``` php
140- $adapter->hook('onRoutingError', function (string $hostname, \Exception $e) {
141- // Log to Sentry
142- Sentry\captureException($e, [
143- 'tags' => ['hostname' => $hostname],
144- 'level' => 'error',
145- ]);
146-
147- // Try fallback region
148- if ($e->getMessage() === 'Function not found') {
149- tryFallbackRegion($hostname);
150- }
151-
152- // Update error metrics
153- $metrics->increment('proxy.routes.errors');
154- });
155- ```
156-
157- ## Registering Multiple Hooks
158-
159- You can register multiple callbacks for the same hook:
160-
161- ``` php
162- // Hook 1: Validation
163- $adapter->hook('beforeRoute', function ($hostname) {
164- validateHostname($hostname);
165- });
166-
167- // Hook 2: Rate limiting
168- $adapter->hook('beforeRoute', function ($hostname) {
169- checkRateLimit($hostname);
170- });
171-
172- // Hook 3: Authentication
173- $adapter->hook('beforeRoute', function ($hostname) {
174- validateJWT();
175- });
176- ```
177-
178- All registered hooks will execute in the order they were registered.
179-
180126## Integration with Appwrite Edge
181127
182- The protocol-proxy can replace the current edge HTTP proxy by using hooks to inject edge-specific logic:
128+ The protocol-proxy can replace the current edge HTTP proxy by using actions to inject edge-specific logic:
183129
184130``` php
185- use Utopia\Proxy\Adapter\HTTP;
186-
187- $adapter = new HTTP($cache, $dbPool);
188-
189- // Hook 1: Resolve backend using K8s runtime registry (REQUIRED)
190- $adapter->hook('resolve', function (string $hostname) use ($runtimeRegistry) {
191- // Edge resolves hostnames to K8s service endpoints
192- $runtime = $runtimeRegistry->get($hostname);
193- if (!$runtime) {
194- throw new \Exception("Runtime not found: {$hostname}");
195- }
196-
197- // Return K8s service endpoint
198- return "{$runtime['projectId']}-{$runtime['deploymentId']}.runtimes.svc.cluster.local:8080";
199- });
200-
201- // Hook 2: Rule resolution and caching
202- $adapter->hook('beforeRoute', function (string $hostname) use ($ruleCache, $sdkForManager) {
203- $rule = $ruleCache->load($hostname);
204- if (!$rule) {
205- $rule = $sdkForManager->getRule($hostname);
206- $ruleCache->save($hostname, $rule);
207- }
208- Context::set('rule', $rule);
209- });
210-
211- // Hook 3: Telemetry and metrics
212- $adapter->hook('afterRoute', function (string $hostname, string $endpoint, $result) use ($telemetry) {
213- $telemetry->record([
214- 'hostname' => $hostname,
215- 'endpoint' => $endpoint,
216- 'cached' => $result->metadata['cached'],
217- 'latency_ms' => $result->metadata['latency_ms'],
218- ]);
219- });
220-
221- // Hook 4: Error logging
222- $adapter->hook('onRoutingError', function (string $hostname, \Exception $e) use ($logger) {
223- $logger->addLog([
224- 'type' => 'error',
225- 'hostname' => $hostname,
226- 'message' => $e->getMessage(),
227- 'trace' => $e->getTraceAsString(),
228- ]);
229- });
131+ use Utopia\Platform\Action;
132+ use Utopia\Proxy\Adapter\HTTP\Swoole as HTTPAdapter;
133+ use Utopia\Proxy\Service\HTTP as HTTPService;
134+
135+ $adapter = new HTTPAdapter();
136+ $service = $adapter->getService() ?? new HTTPService();
137+
138+ // Resolve backend using K8s runtime registry (REQUIRED)
139+ $service->addAction('resolve', (new class extends Action {})
140+ ->callback(function (string $hostname) use ($runtimeRegistry): string {
141+ $runtime = $runtimeRegistry->get($hostname);
142+ if (!$runtime) {
143+ throw new \Exception("Runtime not found: {$hostname}");
144+ }
145+ return "{$runtime['projectId']}-{$runtime['deploymentId']}.runtimes.svc.cluster.local:8080";
146+ }));
147+
148+ // Rule resolution and caching
149+ $service->addAction('resolveRule', (new class extends Action {})
150+ ->setType(Action::TYPE_INIT)
151+ ->callback(function (string $hostname) use ($ruleCache, $sdkForManager) {
152+ $rule = $ruleCache->load($hostname);
153+ if (!$rule) {
154+ $rule = $sdkForManager->getRule($hostname);
155+ $ruleCache->save($hostname, $rule);
156+ }
157+ Context::set('rule', $rule);
158+ }));
159+
160+ // Telemetry and metrics
161+ $service->addAction('telemetry', (new class extends Action {})
162+ ->setType(Action::TYPE_SHUTDOWN)
163+ ->callback(function (string $hostname, string $endpoint, $result) use ($telemetry) {
164+ $telemetry->record([
165+ 'hostname' => $hostname,
166+ 'endpoint' => $endpoint,
167+ 'cached' => $result->metadata['cached'],
168+ 'latency_ms' => $result->metadata['latency_ms'],
169+ ]);
170+ }));
171+
172+ // Error logging
173+ $service->addAction('routeError', (new class extends Action {})
174+ ->setType(Action::TYPE_ERROR)
175+ ->callback(function (string $hostname, \Exception $e) use ($logger) {
176+ $logger->addLog([
177+ 'type' => 'error',
178+ 'hostname' => $hostname,
179+ 'message' => $e->getMessage(),
180+ 'trace' => $e->getTraceAsString(),
181+ ]);
182+ }));
183+
184+ $adapter->setService($service);
230185```
231186
232187## Performance Considerations
233188
234- - ** Hooks are synchronous** - They execute inline during routing
235- - ** Keep hooks fast** - Slow hooks will impact overall proxy performance
189+ - ** Actions are synchronous** - They execute inline during routing
190+ - ** Keep actions fast** - Slow actions will impact overall proxy performance
236191- ** Use async operations** - For non-critical work (logging, metrics), consider using Swoole coroutines or queues
237- - ** Avoid heavy I/O** - Database queries and API calls in hooks should be cached or batched
192+ - ** Avoid heavy I/O** - Database queries and API calls in actions should be cached or batched
238193
239194## Best Practices
240195
241- 1 . ** Fail fast** - Throw exceptions early in ` beforeRoute ` to avoid unnecessary work
242- 2 . ** Keep it simple** - Each hook should do one thing well
243- 3 . ** Handle errors** - Wrap hook logic in try/catch to prevent cascading failures
244- 4 . ** Document hooks ** - Clearly document what each hook does and why
245- 5 . ** Test hooks ** - Write unit tests for hook callbacks
246- 6 . ** Monitor performance** - Track hook execution time to identify bottlenecks
196+ 1 . ** Fail fast** - Throw exceptions early in init actions to avoid unnecessary work
197+ 2 . ** Keep it simple** - Each action should do one thing well
198+ 3 . ** Handle errors** - Wrap action logic in try/catch to prevent cascading failures
199+ 4 . ** Document actions ** - Clearly document what each action does and why
200+ 5 . ** Test actions ** - Write unit tests for action callbacks
201+ 6 . ** Monitor performance** - Track action execution time to identify bottlenecks
247202
248203## Example: Complete Edge Integration
249204
250- See ` examples/http-edge-integration.php ` for a complete example of how Appwrite Edge can integrate with the protocol-proxy using hooks .
205+ See ` examples/http-edge-integration.php ` for a complete example of how Appwrite Edge can integrate with the protocol-proxy using actions .
0 commit comments