rubyn-code 0.1.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/README.md +620 -0
- data/db/migrations/000_create_schema_migrations.sql +4 -0
- data/db/migrations/001_create_sessions.sql +16 -0
- data/db/migrations/002_create_messages.sql +16 -0
- data/db/migrations/003_create_tasks.sql +17 -0
- data/db/migrations/004_create_task_dependencies.sql +8 -0
- data/db/migrations/005_create_memories.sql +44 -0
- data/db/migrations/006_create_cost_records.sql +16 -0
- data/db/migrations/007_create_hooks.sql +12 -0
- data/db/migrations/008_create_skills_cache.sql +8 -0
- data/db/migrations/009_create_teams.sql +27 -0
- data/db/migrations/010_create_instincts.sql +15 -0
- data/exe/rubyn-code +6 -0
- data/lib/rubyn_code/agent/conversation.rb +193 -0
- data/lib/rubyn_code/agent/loop.rb +517 -0
- data/lib/rubyn_code/agent/loop_detector.rb +78 -0
- data/lib/rubyn_code/auth/oauth.rb +174 -0
- data/lib/rubyn_code/auth/server.rb +126 -0
- data/lib/rubyn_code/auth/token_store.rb +153 -0
- data/lib/rubyn_code/autonomous/daemon.rb +233 -0
- data/lib/rubyn_code/autonomous/idle_poller.rb +111 -0
- data/lib/rubyn_code/autonomous/task_claimer.rb +100 -0
- data/lib/rubyn_code/background/job.rb +19 -0
- data/lib/rubyn_code/background/notifier.rb +44 -0
- data/lib/rubyn_code/background/worker.rb +146 -0
- data/lib/rubyn_code/cli/app.rb +118 -0
- data/lib/rubyn_code/cli/input_handler.rb +79 -0
- data/lib/rubyn_code/cli/renderer.rb +205 -0
- data/lib/rubyn_code/cli/repl.rb +519 -0
- data/lib/rubyn_code/cli/spinner.rb +100 -0
- data/lib/rubyn_code/cli/stream_formatter.rb +149 -0
- data/lib/rubyn_code/config/defaults.rb +43 -0
- data/lib/rubyn_code/config/project_config.rb +120 -0
- data/lib/rubyn_code/config/settings.rb +127 -0
- data/lib/rubyn_code/context/auto_compact.rb +81 -0
- data/lib/rubyn_code/context/compactor.rb +89 -0
- data/lib/rubyn_code/context/manager.rb +91 -0
- data/lib/rubyn_code/context/manual_compact.rb +87 -0
- data/lib/rubyn_code/context/micro_compact.rb +135 -0
- data/lib/rubyn_code/db/connection.rb +176 -0
- data/lib/rubyn_code/db/migrator.rb +146 -0
- data/lib/rubyn_code/db/schema.rb +106 -0
- data/lib/rubyn_code/hooks/built_in.rb +124 -0
- data/lib/rubyn_code/hooks/registry.rb +99 -0
- data/lib/rubyn_code/hooks/runner.rb +88 -0
- data/lib/rubyn_code/hooks/user_hooks.rb +90 -0
- data/lib/rubyn_code/learning/extractor.rb +191 -0
- data/lib/rubyn_code/learning/injector.rb +138 -0
- data/lib/rubyn_code/learning/instinct.rb +172 -0
- data/lib/rubyn_code/llm/client.rb +218 -0
- data/lib/rubyn_code/llm/message_builder.rb +116 -0
- data/lib/rubyn_code/llm/streaming.rb +203 -0
- data/lib/rubyn_code/mcp/client.rb +139 -0
- data/lib/rubyn_code/mcp/config.rb +83 -0
- data/lib/rubyn_code/mcp/sse_transport.rb +225 -0
- data/lib/rubyn_code/mcp/stdio_transport.rb +196 -0
- data/lib/rubyn_code/mcp/tool_bridge.rb +164 -0
- data/lib/rubyn_code/memory/models.rb +62 -0
- data/lib/rubyn_code/memory/search.rb +181 -0
- data/lib/rubyn_code/memory/session_persistence.rb +194 -0
- data/lib/rubyn_code/memory/store.rb +199 -0
- data/lib/rubyn_code/observability/budget_enforcer.rb +159 -0
- data/lib/rubyn_code/observability/cost_calculator.rb +61 -0
- data/lib/rubyn_code/observability/models.rb +29 -0
- data/lib/rubyn_code/observability/token_counter.rb +42 -0
- data/lib/rubyn_code/observability/usage_reporter.rb +140 -0
- data/lib/rubyn_code/output/diff_renderer.rb +212 -0
- data/lib/rubyn_code/output/formatter.rb +120 -0
- data/lib/rubyn_code/permissions/deny_list.rb +49 -0
- data/lib/rubyn_code/permissions/policy.rb +59 -0
- data/lib/rubyn_code/permissions/prompter.rb +80 -0
- data/lib/rubyn_code/permissions/tier.rb +22 -0
- data/lib/rubyn_code/protocols/interrupt_handler.rb +95 -0
- data/lib/rubyn_code/protocols/plan_approval.rb +67 -0
- data/lib/rubyn_code/protocols/shutdown_handshake.rb +109 -0
- data/lib/rubyn_code/skills/catalog.rb +70 -0
- data/lib/rubyn_code/skills/document.rb +80 -0
- data/lib/rubyn_code/skills/loader.rb +57 -0
- data/lib/rubyn_code/sub_agents/runner.rb +168 -0
- data/lib/rubyn_code/sub_agents/summarizer.rb +57 -0
- data/lib/rubyn_code/tasks/dag.rb +208 -0
- data/lib/rubyn_code/tasks/manager.rb +212 -0
- data/lib/rubyn_code/tasks/models.rb +31 -0
- data/lib/rubyn_code/teams/mailbox.rb +128 -0
- data/lib/rubyn_code/teams/manager.rb +175 -0
- data/lib/rubyn_code/teams/teammate.rb +38 -0
- data/lib/rubyn_code/tools/background_run.rb +41 -0
- data/lib/rubyn_code/tools/base.rb +84 -0
- data/lib/rubyn_code/tools/bash.rb +81 -0
- data/lib/rubyn_code/tools/bundle_add.rb +53 -0
- data/lib/rubyn_code/tools/bundle_install.rb +41 -0
- data/lib/rubyn_code/tools/compact.rb +57 -0
- data/lib/rubyn_code/tools/db_migrate.rb +52 -0
- data/lib/rubyn_code/tools/edit_file.rb +49 -0
- data/lib/rubyn_code/tools/executor.rb +62 -0
- data/lib/rubyn_code/tools/git_commit.rb +97 -0
- data/lib/rubyn_code/tools/git_diff.rb +61 -0
- data/lib/rubyn_code/tools/git_log.rb +59 -0
- data/lib/rubyn_code/tools/git_status.rb +59 -0
- data/lib/rubyn_code/tools/glob.rb +44 -0
- data/lib/rubyn_code/tools/grep.rb +81 -0
- data/lib/rubyn_code/tools/load_skill.rb +41 -0
- data/lib/rubyn_code/tools/memory_search.rb +77 -0
- data/lib/rubyn_code/tools/memory_write.rb +52 -0
- data/lib/rubyn_code/tools/rails_generate.rb +54 -0
- data/lib/rubyn_code/tools/read_file.rb +38 -0
- data/lib/rubyn_code/tools/read_inbox.rb +64 -0
- data/lib/rubyn_code/tools/registry.rb +48 -0
- data/lib/rubyn_code/tools/review_pr.rb +145 -0
- data/lib/rubyn_code/tools/run_specs.rb +75 -0
- data/lib/rubyn_code/tools/schema.rb +59 -0
- data/lib/rubyn_code/tools/send_message.rb +53 -0
- data/lib/rubyn_code/tools/spawn_agent.rb +154 -0
- data/lib/rubyn_code/tools/spawn_teammate.rb +168 -0
- data/lib/rubyn_code/tools/task.rb +148 -0
- data/lib/rubyn_code/tools/web_fetch.rb +108 -0
- data/lib/rubyn_code/tools/web_search.rb +196 -0
- data/lib/rubyn_code/tools/write_file.rb +30 -0
- data/lib/rubyn_code/version.rb +5 -0
- data/lib/rubyn_code.rb +203 -0
- data/skills/code_quality/fits_in_your_head.md +189 -0
- data/skills/code_quality/naming_conventions.md +213 -0
- data/skills/code_quality/null_object.md +205 -0
- data/skills/code_quality/technical_debt.md +135 -0
- data/skills/code_quality/value_objects.md +216 -0
- data/skills/code_quality/yagni.md +176 -0
- data/skills/design_patterns/adapter.md +191 -0
- data/skills/design_patterns/bridge_memento_visitor.md +254 -0
- data/skills/design_patterns/builder.md +158 -0
- data/skills/design_patterns/command.md +126 -0
- data/skills/design_patterns/composite.md +147 -0
- data/skills/design_patterns/decorator.md +204 -0
- data/skills/design_patterns/facade.md +133 -0
- data/skills/design_patterns/factory_method.md +169 -0
- data/skills/design_patterns/iterator.md +116 -0
- data/skills/design_patterns/mediator.md +133 -0
- data/skills/design_patterns/observer.md +177 -0
- data/skills/design_patterns/proxy.md +140 -0
- data/skills/design_patterns/singleton.md +124 -0
- data/skills/design_patterns/state.md +207 -0
- data/skills/design_patterns/strategy.md +127 -0
- data/skills/design_patterns/template_method.md +173 -0
- data/skills/gems/devise.md +365 -0
- data/skills/gems/dry_rb.md +186 -0
- data/skills/gems/factory_bot.md +268 -0
- data/skills/gems/faraday.md +263 -0
- data/skills/gems/graphql_ruby.md +514 -0
- data/skills/gems/pundit.md +446 -0
- data/skills/gems/redis.md +219 -0
- data/skills/gems/rubocop.md +257 -0
- data/skills/gems/sidekiq.md +360 -0
- data/skills/gems/stripe.md +224 -0
- data/skills/minitest/assertions.md +185 -0
- data/skills/minitest/fixtures.md +238 -0
- data/skills/minitest/integration_tests.md +210 -0
- data/skills/minitest/mailers_and_jobs.md +218 -0
- data/skills/minitest/mocking_stubbing.md +202 -0
- data/skills/minitest/service_tests_and_performance.md +246 -0
- data/skills/minitest/structure_and_conventions.md +169 -0
- data/skills/minitest/system_tests.md +237 -0
- data/skills/rails/action_cable.md +160 -0
- data/skills/rails/active_record_basics.md +174 -0
- data/skills/rails/active_storage.md +242 -0
- data/skills/rails/api_design.md +212 -0
- data/skills/rails/associations.md +182 -0
- data/skills/rails/background_jobs.md +212 -0
- data/skills/rails/caching.md +158 -0
- data/skills/rails/callbacks.md +135 -0
- data/skills/rails/concerns_controllers.md +218 -0
- data/skills/rails/concerns_models.md +280 -0
- data/skills/rails/controllers.md +190 -0
- data/skills/rails/engines.md +201 -0
- data/skills/rails/form_objects.md +168 -0
- data/skills/rails/hotwire.md +229 -0
- data/skills/rails/internationalization.md +192 -0
- data/skills/rails/logging.md +198 -0
- data/skills/rails/mailers.md +180 -0
- data/skills/rails/migrations.md +200 -0
- data/skills/rails/multitenancy.md +207 -0
- data/skills/rails/n_plus_one.md +151 -0
- data/skills/rails/presenters.md +244 -0
- data/skills/rails/query_objects.md +177 -0
- data/skills/rails/routing.md +194 -0
- data/skills/rails/scopes.md +187 -0
- data/skills/rails/security.md +233 -0
- data/skills/rails/serializers.md +243 -0
- data/skills/rails/service_objects.md +184 -0
- data/skills/rails/testing_strategy.md +258 -0
- data/skills/rails/validations.md +206 -0
- data/skills/refactoring/code_smells.md +251 -0
- data/skills/refactoring/command_query_separation.md +166 -0
- data/skills/refactoring/encapsulate_collection.md +125 -0
- data/skills/refactoring/extract_class.md +138 -0
- data/skills/refactoring/extract_method.md +185 -0
- data/skills/refactoring/replace_conditional.md +211 -0
- data/skills/refactoring/value_objects.md +246 -0
- data/skills/rspec/build_stubbed.md +199 -0
- data/skills/rspec/factory_design.md +206 -0
- data/skills/rspec/let_vs_let_bang.md +161 -0
- data/skills/rspec/mocking_stubbing.md +209 -0
- data/skills/rspec/request_specs.md +212 -0
- data/skills/rspec/service_specs.md +262 -0
- data/skills/rspec/shared_examples.md +244 -0
- data/skills/rspec/system_specs.md +286 -0
- data/skills/rspec/test_performance.md +215 -0
- data/skills/ruby/blocks_procs_lambdas.md +204 -0
- data/skills/ruby/classes.md +155 -0
- data/skills/ruby/concurrency.md +194 -0
- data/skills/ruby/data_struct_openstruct.md +158 -0
- data/skills/ruby/debugging_profiling.md +204 -0
- data/skills/ruby/enumerable_patterns.md +168 -0
- data/skills/ruby/exception_handling.md +199 -0
- data/skills/ruby/file_io.md +217 -0
- data/skills/ruby/hashes.md +195 -0
- data/skills/ruby/metaprogramming.md +170 -0
- data/skills/ruby/modules.md +210 -0
- data/skills/ruby/pattern_matching.md +177 -0
- data/skills/ruby/regular_expressions.md +166 -0
- data/skills/ruby/result_objects.md +200 -0
- data/skills/ruby/strings.md +177 -0
- data/skills/ruby_project/bundler_dependencies.md +181 -0
- data/skills/ruby_project/cli_tools.md +224 -0
- data/skills/ruby_project/rake_tasks.md +146 -0
- data/skills/ruby_project/structure.md +261 -0
- data/skills/sinatra/application_structure.md +241 -0
- data/skills/sinatra/middleware_and_deployment.md +221 -0
- data/skills/sinatra/testing.md +233 -0
- data/skills/solid/dependency_inversion.md +195 -0
- data/skills/solid/interface_segregation.md +237 -0
- data/skills/solid/liskov_substitution.md +263 -0
- data/skills/solid/open_closed.md +212 -0
- data/skills/solid/single_responsibility.md +183 -0
- metadata +397 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# Gem: Faraday
|
|
2
|
+
|
|
3
|
+
## What It Is
|
|
4
|
+
|
|
5
|
+
Faraday is the standard Ruby HTTP client library. It provides a consistent interface for making HTTP requests with middleware for logging, retries, JSON parsing, authentication, and error handling. It's adapter-agnostic — you can swap the backend (Net::HTTP, Typhoeus, Patron) without changing your code.
|
|
6
|
+
|
|
7
|
+
## Setup Done Right
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# Build a reusable client with middleware
|
|
11
|
+
class AnthropicClient
|
|
12
|
+
BASE_URL = "https://api.anthropic.com".freeze
|
|
13
|
+
|
|
14
|
+
def initialize(api_key: ENV.fetch("ANTHROPIC_API_KEY"))
|
|
15
|
+
@conn = Faraday.new(url: BASE_URL) do |f|
|
|
16
|
+
f.request :json # Encode request body as JSON
|
|
17
|
+
f.response :json # Parse response body as JSON
|
|
18
|
+
f.response :raise_error # Raise on 4xx/5xx responses
|
|
19
|
+
f.request :retry, { # Retry on transient failures
|
|
20
|
+
max: 3,
|
|
21
|
+
interval: 0.5,
|
|
22
|
+
interval_randomness: 0.5,
|
|
23
|
+
backoff_factor: 2,
|
|
24
|
+
retry_statuses: [429, 500, 502, 503],
|
|
25
|
+
methods: %i[get post],
|
|
26
|
+
retry_block: ->(env, opts, retries, exc) {
|
|
27
|
+
Rails.logger.warn("[Anthropic] Retry #{retries}: #{exc&.message}")
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
f.request :authorization, "x-api-key", api_key
|
|
31
|
+
f.headers["anthropic-version"] = "2023-06-01"
|
|
32
|
+
f.options.timeout = 60 # Read timeout
|
|
33
|
+
f.options.open_timeout = 10 # Connection timeout
|
|
34
|
+
f.adapter Faraday.default_adapter
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def complete(messages, model:, max_tokens:, system: nil)
|
|
39
|
+
body = {
|
|
40
|
+
model: model,
|
|
41
|
+
max_tokens: max_tokens,
|
|
42
|
+
messages: messages
|
|
43
|
+
}
|
|
44
|
+
body[:system] = system if system
|
|
45
|
+
|
|
46
|
+
response = @conn.post("/v1/messages", body)
|
|
47
|
+
response.body
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Gotcha #1: Middleware Order Matters
|
|
53
|
+
|
|
54
|
+
Faraday middleware runs in the order declared for requests (top to bottom) and reverse order for responses (bottom to top). Getting this wrong causes subtle bugs.
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
# WRONG: response :json is before response :raise_error
|
|
58
|
+
Faraday.new(url: BASE_URL) do |f|
|
|
59
|
+
f.request :json
|
|
60
|
+
f.response :json # Parses response FIRST
|
|
61
|
+
f.response :raise_error # Then checks status — but the body is already parsed
|
|
62
|
+
# If the API returns 500 with non-JSON body, :json middleware chokes
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# RIGHT: raise_error runs before json parsing (remember: response middleware is reversed)
|
|
66
|
+
Faraday.new(url: BASE_URL) do |f|
|
|
67
|
+
f.request :json # Encode request as JSON
|
|
68
|
+
f.response :raise_error # Check status FIRST (this actually runs AFTER :json response)
|
|
69
|
+
f.response :json # Then parse response body
|
|
70
|
+
# Wait — this is also wrong! Let me explain...
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# ACTUALLY RIGHT: In Faraday, response middleware executes in REVERSE order
|
|
74
|
+
# So if you want raise_error to run AFTER json parsing:
|
|
75
|
+
Faraday.new(url: BASE_URL) do |f|
|
|
76
|
+
f.request :json
|
|
77
|
+
f.response :json # Parses body first (runs second in reverse order)
|
|
78
|
+
f.response :raise_error # Then raises if status is bad (runs first in reverse order)
|
|
79
|
+
# No wait — raise_error runs BEFORE json in reverse order
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# THE ACTUAL CORRECT ORDER:
|
|
83
|
+
Faraday.new(url: BASE_URL) do |f|
|
|
84
|
+
f.request :json
|
|
85
|
+
f.response :raise_error # Declared first = runs LAST for responses
|
|
86
|
+
f.response :json # Declared second = runs FIRST for responses
|
|
87
|
+
# So: response arrives → json parses it → raise_error checks status
|
|
88
|
+
# If response is 500, raise_error sees parsed body and raises with details
|
|
89
|
+
end
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**The trap:** The mental model is confusing because request middleware runs top-to-bottom but response middleware runs bottom-to-top. When in doubt, test with a failing request and check which error you get.
|
|
93
|
+
|
|
94
|
+
Simplest rule: **put `:raise_error` ABOVE `:json`** in the middleware stack.
|
|
95
|
+
|
|
96
|
+
## Gotcha #2: Timeouts — Set Them or Hang Forever
|
|
97
|
+
|
|
98
|
+
Default Faraday has no timeout. A hung server means your Ruby process hangs forever, tying up a web worker or Sidekiq thread.
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
# WRONG: No timeouts — will hang indefinitely
|
|
102
|
+
conn = Faraday.new(url: "https://slow-api.example.com")
|
|
103
|
+
response = conn.get("/data") # Waits forever if server doesn't respond
|
|
104
|
+
|
|
105
|
+
# RIGHT: Always set timeouts
|
|
106
|
+
conn = Faraday.new(url: "https://api.example.com") do |f|
|
|
107
|
+
f.options.timeout = 30 # Total read timeout (seconds)
|
|
108
|
+
f.options.open_timeout = 5 # Connection timeout (seconds)
|
|
109
|
+
f.options.write_timeout = 10 # Write timeout (seconds) — Ruby 2.6+
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Per-request timeout override
|
|
113
|
+
response = conn.get("/data") do |req|
|
|
114
|
+
req.options.timeout = 5 # This specific request times out faster
|
|
115
|
+
end
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**The trap:** Your app works fine for weeks. Then the external API has a slowdown. Without timeouts, your web workers all hang waiting for responses, your request queue fills up, and your entire app goes down — not just the feature that calls the API.
|
|
119
|
+
|
|
120
|
+
## Gotcha #3: The `raise_error` Middleware
|
|
121
|
+
|
|
122
|
+
Without `raise_error`, Faraday returns the response object even on 4xx/5xx — it does NOT raise an exception.
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
# WRONG: Assuming Faraday raises on errors
|
|
126
|
+
conn = Faraday.new(url: "https://api.example.com")
|
|
127
|
+
response = conn.get("/missing-resource")
|
|
128
|
+
# response.status is 404, but NO exception raised
|
|
129
|
+
# The code continues with a 404 response and breaks later
|
|
130
|
+
|
|
131
|
+
data = response.body["results"] # nil — body is an error page, not JSON
|
|
132
|
+
data.each { |r| process(r) } # NoMethodError: undefined method 'each' for nil
|
|
133
|
+
|
|
134
|
+
# RIGHT: Use raise_error middleware
|
|
135
|
+
conn = Faraday.new(url: "https://api.example.com") do |f|
|
|
136
|
+
f.response :raise_error
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
begin
|
|
140
|
+
response = conn.get("/missing-resource")
|
|
141
|
+
rescue Faraday::ResourceNotFound => e
|
|
142
|
+
Rails.logger.warn("Resource not found: #{e.message}")
|
|
143
|
+
nil
|
|
144
|
+
rescue Faraday::ClientError => e # 4xx errors
|
|
145
|
+
Rails.logger.error("Client error: #{e.message}")
|
|
146
|
+
raise
|
|
147
|
+
rescue Faraday::ServerError => e # 5xx errors
|
|
148
|
+
Rails.logger.error("Server error: #{e.message}")
|
|
149
|
+
raise
|
|
150
|
+
end
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**Error class hierarchy:**
|
|
154
|
+
```
|
|
155
|
+
Faraday::Error
|
|
156
|
+
├── Faraday::ConnectionFailed # Network unreachable, DNS failure
|
|
157
|
+
├── Faraday::TimeoutError # Read/open timeout
|
|
158
|
+
├── Faraday::ClientError # 4xx responses
|
|
159
|
+
│ ├── Faraday::BadRequestError # 400
|
|
160
|
+
│ ├── Faraday::UnauthorizedError # 401
|
|
161
|
+
│ ├── Faraday::ForbiddenError # 403
|
|
162
|
+
│ ├── Faraday::ResourceNotFound # 404
|
|
163
|
+
│ ├── Faraday::ProxyAuthError # 407
|
|
164
|
+
│ ├── Faraday::ConflictError # 409
|
|
165
|
+
│ ├── Faraday::UnprocessableEntityError # 422
|
|
166
|
+
│ └── Faraday::TooManyRequestsError # 429
|
|
167
|
+
└── Faraday::ServerError # 5xx responses
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Gotcha #4: Retry Middleware Configuration
|
|
171
|
+
|
|
172
|
+
The retry middleware only retries idempotent methods (GET, HEAD, OPTIONS) by default. POST requests are NOT retried unless you configure it.
|
|
173
|
+
|
|
174
|
+
```ruby
|
|
175
|
+
# WRONG: retry only works on GET by default
|
|
176
|
+
Faraday.new do |f|
|
|
177
|
+
f.request :retry, max: 3
|
|
178
|
+
# POST /v1/messages will NOT be retried on timeout
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# RIGHT: Explicitly include POST if your API is idempotent
|
|
182
|
+
Faraday.new do |f|
|
|
183
|
+
f.request :retry, {
|
|
184
|
+
max: 3,
|
|
185
|
+
methods: %i[get post], # Include POST
|
|
186
|
+
retry_statuses: [429, 500, 502, 503],
|
|
187
|
+
exceptions: [
|
|
188
|
+
Faraday::TimeoutError,
|
|
189
|
+
Faraday::ConnectionFailed,
|
|
190
|
+
Faraday::RetriableResponse # Required for retry_statuses to work
|
|
191
|
+
]
|
|
192
|
+
}
|
|
193
|
+
end
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**The trap:** You add retry middleware for your AI API calls. GET requests retry fine, but POST requests to Claude never retry on 429 (rate limit). You need `methods: %i[get post]` AND `Faraday::RetriableResponse` in the exceptions list for status-based retries to work.
|
|
197
|
+
|
|
198
|
+
## Gotcha #5: JSON Parsing Failures
|
|
199
|
+
|
|
200
|
+
The `:json` response middleware silently returns the raw string body if JSON parsing fails. Your code expects a Hash but gets a String.
|
|
201
|
+
|
|
202
|
+
```ruby
|
|
203
|
+
# API returns HTML error page instead of JSON
|
|
204
|
+
response = conn.get("/api/data")
|
|
205
|
+
response.body # "<html><body>502 Bad Gateway</body></html>" — not a Hash!
|
|
206
|
+
response.body["data"] # Returns "a" (String#[] with string key)... not nil!
|
|
207
|
+
|
|
208
|
+
# RIGHT: Check response content type or rescue parse errors
|
|
209
|
+
response = conn.get("/api/data")
|
|
210
|
+
unless response.headers["content-type"]&.include?("application/json")
|
|
211
|
+
raise "Unexpected response format: #{response.headers['content-type']}"
|
|
212
|
+
end
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Gotcha #6: Streaming Responses
|
|
216
|
+
|
|
217
|
+
For AI APIs that stream responses (SSE), you need to handle the response body differently.
|
|
218
|
+
|
|
219
|
+
```ruby
|
|
220
|
+
# Streaming with Faraday
|
|
221
|
+
def stream_completion(messages, &block)
|
|
222
|
+
@conn.post("/v1/messages") do |req|
|
|
223
|
+
req.body = {
|
|
224
|
+
model: "claude-haiku-4-5-20251001",
|
|
225
|
+
max_tokens: 4096,
|
|
226
|
+
messages: messages,
|
|
227
|
+
stream: true
|
|
228
|
+
}.to_json
|
|
229
|
+
req.options.on_data = proc do |chunk, overall_received_bytes, env|
|
|
230
|
+
# chunk is a raw string, possibly multiple SSE events
|
|
231
|
+
chunk.each_line do |line|
|
|
232
|
+
next unless line.start_with?("data: ")
|
|
233
|
+
data = line.sub("data: ", "").strip
|
|
234
|
+
next if data == "[DONE]"
|
|
235
|
+
block.call(JSON.parse(data))
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Usage
|
|
242
|
+
stream_completion(messages) do |event|
|
|
243
|
+
print event.dig("delta", "text")
|
|
244
|
+
end
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## Do's and Don'ts Summary
|
|
248
|
+
|
|
249
|
+
**DO:**
|
|
250
|
+
- Always set `timeout` and `open_timeout` on every connection
|
|
251
|
+
- Use `raise_error` middleware so HTTP errors become Ruby exceptions
|
|
252
|
+
- Configure retry middleware explicitly — include POST methods if API is idempotent
|
|
253
|
+
- Wrap Faraday connections in client classes (Adapter pattern)
|
|
254
|
+
- Log requests and responses in development
|
|
255
|
+
- Rescue specific Faraday error classes, not generic `StandardError`
|
|
256
|
+
|
|
257
|
+
**DON'T:**
|
|
258
|
+
- Don't use Faraday without timeouts — one hung request can take down your app
|
|
259
|
+
- Don't assume response body is JSON — check content type or handle parse failures
|
|
260
|
+
- Don't forget `Faraday::RetriableResponse` in retry exceptions when using `retry_statuses`
|
|
261
|
+
- Don't create a new Faraday connection per request — reuse connections
|
|
262
|
+
- Don't put API keys directly in connection setup — use ENV or Rails credentials
|
|
263
|
+
- Don't ignore middleware order — it's the #1 source of confusing bugs
|