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.
Files changed (235) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +620 -0
  4. data/db/migrations/000_create_schema_migrations.sql +4 -0
  5. data/db/migrations/001_create_sessions.sql +16 -0
  6. data/db/migrations/002_create_messages.sql +16 -0
  7. data/db/migrations/003_create_tasks.sql +17 -0
  8. data/db/migrations/004_create_task_dependencies.sql +8 -0
  9. data/db/migrations/005_create_memories.sql +44 -0
  10. data/db/migrations/006_create_cost_records.sql +16 -0
  11. data/db/migrations/007_create_hooks.sql +12 -0
  12. data/db/migrations/008_create_skills_cache.sql +8 -0
  13. data/db/migrations/009_create_teams.sql +27 -0
  14. data/db/migrations/010_create_instincts.sql +15 -0
  15. data/exe/rubyn-code +6 -0
  16. data/lib/rubyn_code/agent/conversation.rb +193 -0
  17. data/lib/rubyn_code/agent/loop.rb +517 -0
  18. data/lib/rubyn_code/agent/loop_detector.rb +78 -0
  19. data/lib/rubyn_code/auth/oauth.rb +174 -0
  20. data/lib/rubyn_code/auth/server.rb +126 -0
  21. data/lib/rubyn_code/auth/token_store.rb +153 -0
  22. data/lib/rubyn_code/autonomous/daemon.rb +233 -0
  23. data/lib/rubyn_code/autonomous/idle_poller.rb +111 -0
  24. data/lib/rubyn_code/autonomous/task_claimer.rb +100 -0
  25. data/lib/rubyn_code/background/job.rb +19 -0
  26. data/lib/rubyn_code/background/notifier.rb +44 -0
  27. data/lib/rubyn_code/background/worker.rb +146 -0
  28. data/lib/rubyn_code/cli/app.rb +118 -0
  29. data/lib/rubyn_code/cli/input_handler.rb +79 -0
  30. data/lib/rubyn_code/cli/renderer.rb +205 -0
  31. data/lib/rubyn_code/cli/repl.rb +519 -0
  32. data/lib/rubyn_code/cli/spinner.rb +100 -0
  33. data/lib/rubyn_code/cli/stream_formatter.rb +149 -0
  34. data/lib/rubyn_code/config/defaults.rb +43 -0
  35. data/lib/rubyn_code/config/project_config.rb +120 -0
  36. data/lib/rubyn_code/config/settings.rb +127 -0
  37. data/lib/rubyn_code/context/auto_compact.rb +81 -0
  38. data/lib/rubyn_code/context/compactor.rb +89 -0
  39. data/lib/rubyn_code/context/manager.rb +91 -0
  40. data/lib/rubyn_code/context/manual_compact.rb +87 -0
  41. data/lib/rubyn_code/context/micro_compact.rb +135 -0
  42. data/lib/rubyn_code/db/connection.rb +176 -0
  43. data/lib/rubyn_code/db/migrator.rb +146 -0
  44. data/lib/rubyn_code/db/schema.rb +106 -0
  45. data/lib/rubyn_code/hooks/built_in.rb +124 -0
  46. data/lib/rubyn_code/hooks/registry.rb +99 -0
  47. data/lib/rubyn_code/hooks/runner.rb +88 -0
  48. data/lib/rubyn_code/hooks/user_hooks.rb +90 -0
  49. data/lib/rubyn_code/learning/extractor.rb +191 -0
  50. data/lib/rubyn_code/learning/injector.rb +138 -0
  51. data/lib/rubyn_code/learning/instinct.rb +172 -0
  52. data/lib/rubyn_code/llm/client.rb +218 -0
  53. data/lib/rubyn_code/llm/message_builder.rb +116 -0
  54. data/lib/rubyn_code/llm/streaming.rb +203 -0
  55. data/lib/rubyn_code/mcp/client.rb +139 -0
  56. data/lib/rubyn_code/mcp/config.rb +83 -0
  57. data/lib/rubyn_code/mcp/sse_transport.rb +225 -0
  58. data/lib/rubyn_code/mcp/stdio_transport.rb +196 -0
  59. data/lib/rubyn_code/mcp/tool_bridge.rb +164 -0
  60. data/lib/rubyn_code/memory/models.rb +62 -0
  61. data/lib/rubyn_code/memory/search.rb +181 -0
  62. data/lib/rubyn_code/memory/session_persistence.rb +194 -0
  63. data/lib/rubyn_code/memory/store.rb +199 -0
  64. data/lib/rubyn_code/observability/budget_enforcer.rb +159 -0
  65. data/lib/rubyn_code/observability/cost_calculator.rb +61 -0
  66. data/lib/rubyn_code/observability/models.rb +29 -0
  67. data/lib/rubyn_code/observability/token_counter.rb +42 -0
  68. data/lib/rubyn_code/observability/usage_reporter.rb +140 -0
  69. data/lib/rubyn_code/output/diff_renderer.rb +212 -0
  70. data/lib/rubyn_code/output/formatter.rb +120 -0
  71. data/lib/rubyn_code/permissions/deny_list.rb +49 -0
  72. data/lib/rubyn_code/permissions/policy.rb +59 -0
  73. data/lib/rubyn_code/permissions/prompter.rb +80 -0
  74. data/lib/rubyn_code/permissions/tier.rb +22 -0
  75. data/lib/rubyn_code/protocols/interrupt_handler.rb +95 -0
  76. data/lib/rubyn_code/protocols/plan_approval.rb +67 -0
  77. data/lib/rubyn_code/protocols/shutdown_handshake.rb +109 -0
  78. data/lib/rubyn_code/skills/catalog.rb +70 -0
  79. data/lib/rubyn_code/skills/document.rb +80 -0
  80. data/lib/rubyn_code/skills/loader.rb +57 -0
  81. data/lib/rubyn_code/sub_agents/runner.rb +168 -0
  82. data/lib/rubyn_code/sub_agents/summarizer.rb +57 -0
  83. data/lib/rubyn_code/tasks/dag.rb +208 -0
  84. data/lib/rubyn_code/tasks/manager.rb +212 -0
  85. data/lib/rubyn_code/tasks/models.rb +31 -0
  86. data/lib/rubyn_code/teams/mailbox.rb +128 -0
  87. data/lib/rubyn_code/teams/manager.rb +175 -0
  88. data/lib/rubyn_code/teams/teammate.rb +38 -0
  89. data/lib/rubyn_code/tools/background_run.rb +41 -0
  90. data/lib/rubyn_code/tools/base.rb +84 -0
  91. data/lib/rubyn_code/tools/bash.rb +81 -0
  92. data/lib/rubyn_code/tools/bundle_add.rb +53 -0
  93. data/lib/rubyn_code/tools/bundle_install.rb +41 -0
  94. data/lib/rubyn_code/tools/compact.rb +57 -0
  95. data/lib/rubyn_code/tools/db_migrate.rb +52 -0
  96. data/lib/rubyn_code/tools/edit_file.rb +49 -0
  97. data/lib/rubyn_code/tools/executor.rb +62 -0
  98. data/lib/rubyn_code/tools/git_commit.rb +97 -0
  99. data/lib/rubyn_code/tools/git_diff.rb +61 -0
  100. data/lib/rubyn_code/tools/git_log.rb +59 -0
  101. data/lib/rubyn_code/tools/git_status.rb +59 -0
  102. data/lib/rubyn_code/tools/glob.rb +44 -0
  103. data/lib/rubyn_code/tools/grep.rb +81 -0
  104. data/lib/rubyn_code/tools/load_skill.rb +41 -0
  105. data/lib/rubyn_code/tools/memory_search.rb +77 -0
  106. data/lib/rubyn_code/tools/memory_write.rb +52 -0
  107. data/lib/rubyn_code/tools/rails_generate.rb +54 -0
  108. data/lib/rubyn_code/tools/read_file.rb +38 -0
  109. data/lib/rubyn_code/tools/read_inbox.rb +64 -0
  110. data/lib/rubyn_code/tools/registry.rb +48 -0
  111. data/lib/rubyn_code/tools/review_pr.rb +145 -0
  112. data/lib/rubyn_code/tools/run_specs.rb +75 -0
  113. data/lib/rubyn_code/tools/schema.rb +59 -0
  114. data/lib/rubyn_code/tools/send_message.rb +53 -0
  115. data/lib/rubyn_code/tools/spawn_agent.rb +154 -0
  116. data/lib/rubyn_code/tools/spawn_teammate.rb +168 -0
  117. data/lib/rubyn_code/tools/task.rb +148 -0
  118. data/lib/rubyn_code/tools/web_fetch.rb +108 -0
  119. data/lib/rubyn_code/tools/web_search.rb +196 -0
  120. data/lib/rubyn_code/tools/write_file.rb +30 -0
  121. data/lib/rubyn_code/version.rb +5 -0
  122. data/lib/rubyn_code.rb +203 -0
  123. data/skills/code_quality/fits_in_your_head.md +189 -0
  124. data/skills/code_quality/naming_conventions.md +213 -0
  125. data/skills/code_quality/null_object.md +205 -0
  126. data/skills/code_quality/technical_debt.md +135 -0
  127. data/skills/code_quality/value_objects.md +216 -0
  128. data/skills/code_quality/yagni.md +176 -0
  129. data/skills/design_patterns/adapter.md +191 -0
  130. data/skills/design_patterns/bridge_memento_visitor.md +254 -0
  131. data/skills/design_patterns/builder.md +158 -0
  132. data/skills/design_patterns/command.md +126 -0
  133. data/skills/design_patterns/composite.md +147 -0
  134. data/skills/design_patterns/decorator.md +204 -0
  135. data/skills/design_patterns/facade.md +133 -0
  136. data/skills/design_patterns/factory_method.md +169 -0
  137. data/skills/design_patterns/iterator.md +116 -0
  138. data/skills/design_patterns/mediator.md +133 -0
  139. data/skills/design_patterns/observer.md +177 -0
  140. data/skills/design_patterns/proxy.md +140 -0
  141. data/skills/design_patterns/singleton.md +124 -0
  142. data/skills/design_patterns/state.md +207 -0
  143. data/skills/design_patterns/strategy.md +127 -0
  144. data/skills/design_patterns/template_method.md +173 -0
  145. data/skills/gems/devise.md +365 -0
  146. data/skills/gems/dry_rb.md +186 -0
  147. data/skills/gems/factory_bot.md +268 -0
  148. data/skills/gems/faraday.md +263 -0
  149. data/skills/gems/graphql_ruby.md +514 -0
  150. data/skills/gems/pundit.md +446 -0
  151. data/skills/gems/redis.md +219 -0
  152. data/skills/gems/rubocop.md +257 -0
  153. data/skills/gems/sidekiq.md +360 -0
  154. data/skills/gems/stripe.md +224 -0
  155. data/skills/minitest/assertions.md +185 -0
  156. data/skills/minitest/fixtures.md +238 -0
  157. data/skills/minitest/integration_tests.md +210 -0
  158. data/skills/minitest/mailers_and_jobs.md +218 -0
  159. data/skills/minitest/mocking_stubbing.md +202 -0
  160. data/skills/minitest/service_tests_and_performance.md +246 -0
  161. data/skills/minitest/structure_and_conventions.md +169 -0
  162. data/skills/minitest/system_tests.md +237 -0
  163. data/skills/rails/action_cable.md +160 -0
  164. data/skills/rails/active_record_basics.md +174 -0
  165. data/skills/rails/active_storage.md +242 -0
  166. data/skills/rails/api_design.md +212 -0
  167. data/skills/rails/associations.md +182 -0
  168. data/skills/rails/background_jobs.md +212 -0
  169. data/skills/rails/caching.md +158 -0
  170. data/skills/rails/callbacks.md +135 -0
  171. data/skills/rails/concerns_controllers.md +218 -0
  172. data/skills/rails/concerns_models.md +280 -0
  173. data/skills/rails/controllers.md +190 -0
  174. data/skills/rails/engines.md +201 -0
  175. data/skills/rails/form_objects.md +168 -0
  176. data/skills/rails/hotwire.md +229 -0
  177. data/skills/rails/internationalization.md +192 -0
  178. data/skills/rails/logging.md +198 -0
  179. data/skills/rails/mailers.md +180 -0
  180. data/skills/rails/migrations.md +200 -0
  181. data/skills/rails/multitenancy.md +207 -0
  182. data/skills/rails/n_plus_one.md +151 -0
  183. data/skills/rails/presenters.md +244 -0
  184. data/skills/rails/query_objects.md +177 -0
  185. data/skills/rails/routing.md +194 -0
  186. data/skills/rails/scopes.md +187 -0
  187. data/skills/rails/security.md +233 -0
  188. data/skills/rails/serializers.md +243 -0
  189. data/skills/rails/service_objects.md +184 -0
  190. data/skills/rails/testing_strategy.md +258 -0
  191. data/skills/rails/validations.md +206 -0
  192. data/skills/refactoring/code_smells.md +251 -0
  193. data/skills/refactoring/command_query_separation.md +166 -0
  194. data/skills/refactoring/encapsulate_collection.md +125 -0
  195. data/skills/refactoring/extract_class.md +138 -0
  196. data/skills/refactoring/extract_method.md +185 -0
  197. data/skills/refactoring/replace_conditional.md +211 -0
  198. data/skills/refactoring/value_objects.md +246 -0
  199. data/skills/rspec/build_stubbed.md +199 -0
  200. data/skills/rspec/factory_design.md +206 -0
  201. data/skills/rspec/let_vs_let_bang.md +161 -0
  202. data/skills/rspec/mocking_stubbing.md +209 -0
  203. data/skills/rspec/request_specs.md +212 -0
  204. data/skills/rspec/service_specs.md +262 -0
  205. data/skills/rspec/shared_examples.md +244 -0
  206. data/skills/rspec/system_specs.md +286 -0
  207. data/skills/rspec/test_performance.md +215 -0
  208. data/skills/ruby/blocks_procs_lambdas.md +204 -0
  209. data/skills/ruby/classes.md +155 -0
  210. data/skills/ruby/concurrency.md +194 -0
  211. data/skills/ruby/data_struct_openstruct.md +158 -0
  212. data/skills/ruby/debugging_profiling.md +204 -0
  213. data/skills/ruby/enumerable_patterns.md +168 -0
  214. data/skills/ruby/exception_handling.md +199 -0
  215. data/skills/ruby/file_io.md +217 -0
  216. data/skills/ruby/hashes.md +195 -0
  217. data/skills/ruby/metaprogramming.md +170 -0
  218. data/skills/ruby/modules.md +210 -0
  219. data/skills/ruby/pattern_matching.md +177 -0
  220. data/skills/ruby/regular_expressions.md +166 -0
  221. data/skills/ruby/result_objects.md +200 -0
  222. data/skills/ruby/strings.md +177 -0
  223. data/skills/ruby_project/bundler_dependencies.md +181 -0
  224. data/skills/ruby_project/cli_tools.md +224 -0
  225. data/skills/ruby_project/rake_tasks.md +146 -0
  226. data/skills/ruby_project/structure.md +261 -0
  227. data/skills/sinatra/application_structure.md +241 -0
  228. data/skills/sinatra/middleware_and_deployment.md +221 -0
  229. data/skills/sinatra/testing.md +233 -0
  230. data/skills/solid/dependency_inversion.md +195 -0
  231. data/skills/solid/interface_segregation.md +237 -0
  232. data/skills/solid/liskov_substitution.md +263 -0
  233. data/skills/solid/open_closed.md +212 -0
  234. data/skills/solid/single_responsibility.md +183 -0
  235. 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