ice-jade 0.6.0 → 0.8.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 +4 -4
- data/README.md +104 -11
- data/bin/mcp_server.rb +551 -0
- data/examples/comparison_getter.rb +193 -0
- data/examples/getter_usage.rb +85 -0
- data/lib/http_getter.rb +159 -0
- data/lib/ice_jade/getter/client.rb +78 -0
- data/lib/ice_jade/getter.rb +3 -0
- data/lib/ice_jade/poster/client.rb +7 -136
- data/lib/ice_jade/transport.rb +179 -0
- data/lib/ice_jade/version.rb +1 -1
- data/lib/ice_jade.rb +1 -0
- metadata +12 -4
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# =============================================================================
|
|
5
|
+
# HttpGetter vs IceJade::Getter 同步对比示例
|
|
6
|
+
# =============================================================================
|
|
7
|
+
#
|
|
8
|
+
# 本文件把两种调用方式并排展示,均面向本地 Cradle YAML 实例,
|
|
9
|
+
# 通过 YAML 配置启动,无需外部网络,直接本地闭环验证。
|
|
10
|
+
#
|
|
11
|
+
# 左侧:HttpGetter —— 独立模块,静态方法,异常驱动
|
|
12
|
+
# 右侧:IceJade::Getter —— 面向对象,实例化复用,Response 驱动
|
|
13
|
+
#
|
|
14
|
+
# =============================================================================
|
|
15
|
+
|
|
16
|
+
require_relative '../lib/http_getter'
|
|
17
|
+
require_relative '../lib/ice_jade'
|
|
18
|
+
|
|
19
|
+
puts "=" * 70
|
|
20
|
+
puts "HttpGetter (独立模块) vs IceJade::Getter (ice-jade gem 分支)"
|
|
21
|
+
puts "目标: 本地 Cradle YAML 实例"
|
|
22
|
+
puts "=" * 70
|
|
23
|
+
|
|
24
|
+
# =============================================================================
|
|
25
|
+
# 启动 Cradle YAML 实例
|
|
26
|
+
# =============================================================================
|
|
27
|
+
config_path = File.join(__dir__, '../config/cradle/poster_test.yml')
|
|
28
|
+
instance = IceJade::Cradle::YAMLServer.new(config_path)
|
|
29
|
+
thread = Thread.new { instance.start }
|
|
30
|
+
sleep 0.5
|
|
31
|
+
|
|
32
|
+
base_url = "http://#{instance.config['host']}:#{instance.config['port']}"
|
|
33
|
+
|
|
34
|
+
# IceJade::Getter 实例(复用配置)
|
|
35
|
+
getter = IceJade::Getter::Client.new(
|
|
36
|
+
base_url: base_url,
|
|
37
|
+
headers: { 'X-Custom-Header' => 'ice-jade' },
|
|
38
|
+
timeout: 30
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
begin
|
|
42
|
+
|
|
43
|
+
# ==========================================================================
|
|
44
|
+
# 场景 1:GET 查询参数回显
|
|
45
|
+
# ==========================================================================
|
|
46
|
+
puts "\n【场景 1】GET → /echo (查询参数)"
|
|
47
|
+
puts "-" * 70
|
|
48
|
+
|
|
49
|
+
# --- HttpGetter ---
|
|
50
|
+
begin
|
|
51
|
+
res = HttpGetter.get_json(
|
|
52
|
+
"#{base_url}/echo",
|
|
53
|
+
{ page: 1, size: 20 },
|
|
54
|
+
{ 'X-Custom-Header' => 'ice-jade' },
|
|
55
|
+
{ timeout: 30 }
|
|
56
|
+
)
|
|
57
|
+
puts "[HttpGetter] Query echoed: #{res.is_a?(Hash) ? res['query'] : res}"
|
|
58
|
+
rescue HttpGetter::HttpError => e
|
|
59
|
+
puts "[HttpGetter] 失败: #{e.message}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# --- IceJade::Getter ---
|
|
63
|
+
resp = getter.get('/echo', params: { page: 1, size: 20 })
|
|
64
|
+
puts "[IceJade::Getter] Query echoed: #{resp.data.is_a?(Hash) ? resp.data['query'] : 'N/A'}"
|
|
65
|
+
puts " success?: #{resp.success?}, code: #{resp.code}"
|
|
66
|
+
|
|
67
|
+
# ==========================================================================
|
|
68
|
+
# 场景 2:GET 自定义请求头回显
|
|
69
|
+
# ==========================================================================
|
|
70
|
+
puts "\n【场景 2】GET → /headers (自定义头)"
|
|
71
|
+
puts "-" * 70
|
|
72
|
+
|
|
73
|
+
# --- HttpGetter ---
|
|
74
|
+
begin
|
|
75
|
+
res = HttpGetter.get_json(
|
|
76
|
+
"#{base_url}/headers",
|
|
77
|
+
{},
|
|
78
|
+
{ 'X-Custom-Header' => 'ice-jade', 'X-Request-ID' => 'uuid-5678' },
|
|
79
|
+
{ timeout: 30 }
|
|
80
|
+
)
|
|
81
|
+
echoed = res.is_a?(Hash) ? res['headers'] : {}
|
|
82
|
+
puts "[HttpGetter] X-Custom-Header: #{echoed['X-Custom-Header']}"
|
|
83
|
+
puts "[HttpGetter] X-Request-Id: #{echoed['X-Request-Id']}"
|
|
84
|
+
rescue HttpGetter::HttpError => e
|
|
85
|
+
puts "[HttpGetter] 失败: #{e.message}"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# --- IceJade::Getter ---
|
|
89
|
+
resp = getter.get('/headers', headers: { 'X-Request-ID' => 'uuid-5678' })
|
|
90
|
+
echoed = resp.data.is_a?(Hash) ? resp.data['headers'] : {}
|
|
91
|
+
puts "[IceJade::Getter] X-Custom-Header: #{echoed['X-Custom-Header']}"
|
|
92
|
+
puts "[IceJade::Getter] X-Request-Id: #{echoed['X-Request-Id']}"
|
|
93
|
+
puts " success?: #{resp.success?}, code: #{resp.code}"
|
|
94
|
+
|
|
95
|
+
# ==========================================================================
|
|
96
|
+
# 场景 3:GET 纯文本(get_text 返回原始字符串,不做 JSON 解析)
|
|
97
|
+
# ==========================================================================
|
|
98
|
+
puts "\n【场景 3】GET 纯文本 → /echo (get_text)"
|
|
99
|
+
puts "-" * 70
|
|
100
|
+
|
|
101
|
+
# --- HttpGetter ---
|
|
102
|
+
begin
|
|
103
|
+
res = HttpGetter.get_text(
|
|
104
|
+
"#{base_url}/echo",
|
|
105
|
+
{ ping: 'pong' },
|
|
106
|
+
{},
|
|
107
|
+
{ timeout: 30 }
|
|
108
|
+
)
|
|
109
|
+
puts "[HttpGetter] 原始字符串: #{res.class} (#{res.to_s[0, 60]}...)"
|
|
110
|
+
rescue HttpGetter::HttpError => e
|
|
111
|
+
puts "[HttpGetter] 失败: #{e.message}"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# --- IceJade::Getter ---
|
|
115
|
+
# Getter 没有 raw 选项,统一返回 Response,data 已解析
|
|
116
|
+
resp = getter.get('/echo', params: { ping: 'pong' })
|
|
117
|
+
puts "[IceJade::Getter] 解析后 data: #{resp.data.class}"
|
|
118
|
+
puts " success?: #{resp.success?}, code: #{resp.code}"
|
|
119
|
+
|
|
120
|
+
# ==========================================================================
|
|
121
|
+
# 场景 4:错误处理(HTTP 404)
|
|
122
|
+
# ==========================================================================
|
|
123
|
+
puts "\n【场景 4】错误处理 → /status/404"
|
|
124
|
+
puts "-" * 70
|
|
125
|
+
|
|
126
|
+
# --- HttpGetter ---
|
|
127
|
+
begin
|
|
128
|
+
HttpGetter.get_json("#{base_url}/status/404", {}, {}, { timeout: 30 })
|
|
129
|
+
rescue HttpGetter::HttpError => e
|
|
130
|
+
puts "[HttpGetter] 捕获异常: #{e.message}"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# --- IceJade::Getter ---
|
|
134
|
+
resp = getter.get('/status/404')
|
|
135
|
+
puts "[IceJade::Getter] 不抛异常,直接检查响应:"
|
|
136
|
+
puts " success?: #{resp.success?}, code: #{resp.code}, message: #{resp.message}"
|
|
137
|
+
|
|
138
|
+
# ==========================================================================
|
|
139
|
+
# 场景 5:通用 get(url 已含 query,params 追加)
|
|
140
|
+
# ==========================================================================
|
|
141
|
+
puts "\n【场景 5】通用 get → /echo?foo=1 (params 追加)"
|
|
142
|
+
puts "-" * 70
|
|
143
|
+
|
|
144
|
+
# --- HttpGetter ---
|
|
145
|
+
begin
|
|
146
|
+
res = HttpGetter.get(
|
|
147
|
+
"#{base_url}/echo?foo=1",
|
|
148
|
+
{ bar: '2' },
|
|
149
|
+
{ 'X-Request-ID' => 'uuid-1234' },
|
|
150
|
+
{ timeout: 30 }
|
|
151
|
+
)
|
|
152
|
+
puts "[HttpGetter] Query echoed: #{res.is_a?(Hash) ? res['query'] : res}"
|
|
153
|
+
rescue HttpGetter::HttpError => e
|
|
154
|
+
puts "[HttpGetter] 失败: #{e.message}"
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# --- IceJade::Getter ---
|
|
158
|
+
resp = getter.get('/echo?foo=1', params: { bar: '2' },
|
|
159
|
+
headers: { 'X-Request-ID' => 'uuid-1234' })
|
|
160
|
+
puts "[IceJade::Getter] Query echoed: #{resp.data.is_a?(Hash) ? resp.data['query'] : 'N/A'}"
|
|
161
|
+
puts " success?: #{resp.success?}, code: #{resp.code}"
|
|
162
|
+
|
|
163
|
+
# ==========================================================================
|
|
164
|
+
# 结构差异速查表
|
|
165
|
+
# ==========================================================================
|
|
166
|
+
puts "\n" + "=" * 70
|
|
167
|
+
puts "结构差异速查表"
|
|
168
|
+
puts "=" * 70
|
|
169
|
+
puts <<~DIFF
|
|
170
|
+
|
|
171
|
+
┌──────────────────┬──────────────────────────┬──────────────────────────┐
|
|
172
|
+
│ 维度 │ HttpGetter │ IceJade::Getter │
|
|
173
|
+
├──────────────────┼──────────────────────────┼──────────────────────────┤
|
|
174
|
+
│ 调用方式 │ 模块静态方法 │ 先 new 实例化再调用 │
|
|
175
|
+
│ URL 处理 │ 只能传完整 URL │ 支持 base_url + 相对路径 │
|
|
176
|
+
│ 参数风格 │ 位置参数 (url,p,h,opts) │ 关键字参数 (params:) │
|
|
177
|
+
│ 配置复用 │ 每次调用都传 headers │ 初始化一次,多次复用 │
|
|
178
|
+
│ 成功返回 │ Hash / String │ IceJade::Response │
|
|
179
|
+
│ 错误处理 │ 抛 TimeoutError/HttpError│ 包装为 Response │
|
|
180
|
+
│ 判断结果 │ begin/rescue │ resp.success? │
|
|
181
|
+
│ 原始文本 │ get_text 直接返回 String │ 统一 Response(data 解析)│
|
|
182
|
+
│ 所属体系 │ 独立脚本 │ ice-jade gem 分支 │
|
|
183
|
+
│ 适合场景 │ 快速脚本、教学、零依赖 │ 正式项目、与 Quantum 混用│
|
|
184
|
+
└──────────────────┴──────────────────────────┴──────────────────────────┘
|
|
185
|
+
|
|
186
|
+
DIFF
|
|
187
|
+
puts "示例运行完毕。"
|
|
188
|
+
|
|
189
|
+
ensure
|
|
190
|
+
instance.stop
|
|
191
|
+
thread.join
|
|
192
|
+
puts "\nCradle YAML 实例已关闭。"
|
|
193
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Getter 用法示例:调用 Cradle YAML 实例
|
|
5
|
+
#
|
|
6
|
+
# 本示例通过 YAML 配置启动定制化的 Cradle 测试实例,
|
|
7
|
+
# 然后用 IceJade::Getter::Client 向它发起 GET 请求,本地闭环验证。
|
|
8
|
+
#
|
|
9
|
+
# Getter 与 Poster 并列:Poster 专责 POST,Getter 专责 GET,
|
|
10
|
+
# 两者复用同一套 IceJade::Transport 底层(URI 拼接、超时重试、Response 包装)。
|
|
11
|
+
|
|
12
|
+
require_relative '../lib/ice_jade'
|
|
13
|
+
|
|
14
|
+
# =============================================================================
|
|
15
|
+
# 1. 启动 Cradle YAML 实例
|
|
16
|
+
# =============================================================================
|
|
17
|
+
puts "=== 启动 Cradle YAML 实例 ==="
|
|
18
|
+
config_path = File.join(__dir__, '../config/cradle/poster_test.yml')
|
|
19
|
+
instance = IceJade::Cradle::YAMLServer.new(config_path)
|
|
20
|
+
thread = Thread.new { instance.start }
|
|
21
|
+
sleep 0.5
|
|
22
|
+
|
|
23
|
+
getter = IceJade::Getter::Client.new(
|
|
24
|
+
base_url: "http://#{instance.config['host']}:#{instance.config['port']}",
|
|
25
|
+
headers: { 'X-Custom-Header' => 'ice-jade' }
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
begin
|
|
29
|
+
|
|
30
|
+
# ------------------------------------------------------------------
|
|
31
|
+
# 1. GET 查询参数回显
|
|
32
|
+
# ------------------------------------------------------------------
|
|
33
|
+
puts "\n=== 1. GET → /echo (查询参数) ==="
|
|
34
|
+
resp = getter.get('/echo', params: { page: 1, size: 20 })
|
|
35
|
+
puts "Success: #{resp.success?}, Code: #{resp.code}, Message: #{resp.message}"
|
|
36
|
+
puts "Query echoed: #{resp.data.is_a?(Hash) ? resp.data['query'] : 'N/A'}"
|
|
37
|
+
|
|
38
|
+
# ------------------------------------------------------------------
|
|
39
|
+
# 2. GET 回显请求头(默认头 + 额外头合并校验)
|
|
40
|
+
# ------------------------------------------------------------------
|
|
41
|
+
puts "\n=== 2. GET → /headers (自定义头) ==="
|
|
42
|
+
resp = getter.get('/headers', headers: { 'X-Request-ID' => 'uuid-5678' })
|
|
43
|
+
puts "Success: #{resp.success?}, Code: #{resp.code}"
|
|
44
|
+
# 注意:Net::HTTP 会将头名规范化为 X-Request-Id(每段首字母大写)
|
|
45
|
+
echoed_headers = resp.data.is_a?(Hash) ? resp.data['headers'] : {}
|
|
46
|
+
puts "X-Custom-Header: #{echoed_headers['X-Custom-Header']}"
|
|
47
|
+
puts "X-Request-Id: #{echoed_headers['X-Request-Id']}"
|
|
48
|
+
|
|
49
|
+
# ------------------------------------------------------------------
|
|
50
|
+
# 3. GET 错误处理(HTTP 404)
|
|
51
|
+
# ------------------------------------------------------------------
|
|
52
|
+
puts "\n=== 3. GET 错误处理 → /status/404 ==="
|
|
53
|
+
resp = getter.get('/status/404')
|
|
54
|
+
puts "Success: #{resp.success?}, Code: #{resp.code}, Message: #{resp.message}"
|
|
55
|
+
|
|
56
|
+
# ------------------------------------------------------------------
|
|
57
|
+
# 4. GET 错误处理(HTTP 500)
|
|
58
|
+
# ------------------------------------------------------------------
|
|
59
|
+
puts "\n=== 4. GET 错误处理 → /status/500 ==="
|
|
60
|
+
resp = getter.get('/status/500')
|
|
61
|
+
puts "Success: #{resp.success?}, Code: #{resp.code}, Message: #{resp.message}"
|
|
62
|
+
|
|
63
|
+
# ------------------------------------------------------------------
|
|
64
|
+
# 5. GET 已含 query 的 URL(params 追加而非覆盖)
|
|
65
|
+
# ------------------------------------------------------------------
|
|
66
|
+
puts "\n=== 5. GET → /echo?foo=1 (params 追加) ==="
|
|
67
|
+
resp = getter.get('/echo?foo=1', params: { bar: '2' })
|
|
68
|
+
puts "Success: #{resp.success?}, Code: #{resp.code}"
|
|
69
|
+
puts "Query echoed: #{resp.data.is_a?(Hash) ? resp.data['query'] : 'N/A'}"
|
|
70
|
+
|
|
71
|
+
# ------------------------------------------------------------------
|
|
72
|
+
# 6. GET 健康检查
|
|
73
|
+
# ------------------------------------------------------------------
|
|
74
|
+
puts "\n=== 6. GET → /health ==="
|
|
75
|
+
resp = getter.get('/health')
|
|
76
|
+
puts "Success: #{resp.success?}, Code: #{resp.code}"
|
|
77
|
+
puts "Status: #{resp.data.is_a?(Hash) ? resp.data['status'] : 'N/A'}"
|
|
78
|
+
|
|
79
|
+
puts "\n=== 所有测试通过 ==="
|
|
80
|
+
|
|
81
|
+
ensure
|
|
82
|
+
instance.stop
|
|
83
|
+
thread.join
|
|
84
|
+
puts "\nCradle YAML 实例已关闭。"
|
|
85
|
+
end
|
data/lib/http_getter.rb
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# http_getter.rb
|
|
2
|
+
# Ruby 纯标准库 HTTP GET 工具模块
|
|
3
|
+
# 零外部依赖,支持查询参数 / JSON / 透传响应
|
|
4
|
+
#
|
|
5
|
+
# =============================================================================
|
|
6
|
+
# 说明:本模块是独立工具脚本,不依赖 ice-jade gem。
|
|
7
|
+
#
|
|
8
|
+
# 如果你正在使用 ice-jade 项目,推荐优先使用 IceJade::Getter::Client
|
|
9
|
+
# (位于 lib/ice_jade/getter/client.rb),原因:
|
|
10
|
+
#
|
|
11
|
+
# - 面向对象:初始化时配置 base_url/headers/timeout,后续复用
|
|
12
|
+
# - 统一响应:返回 IceJade::Response(ok, code, message, data, raw)
|
|
13
|
+
# - 异常友好:超时/HTTP 错误均包装为 Response,不抛异常
|
|
14
|
+
# - 与 Quantum 分支共享同一套响应体系
|
|
15
|
+
#
|
|
16
|
+
# HttpGetter 适合:
|
|
17
|
+
# - 不想引入任何 gem 依赖的独立脚本
|
|
18
|
+
# - 快速测试、一次性请求
|
|
19
|
+
# - 学习/教学用途,代码更扁平直观
|
|
20
|
+
#
|
|
21
|
+
# 对比示例见:examples/comparison_getter.rb
|
|
22
|
+
# =============================================================================
|
|
23
|
+
|
|
24
|
+
require 'net/http'
|
|
25
|
+
require 'json'
|
|
26
|
+
require 'uri'
|
|
27
|
+
|
|
28
|
+
module HttpGetter
|
|
29
|
+
class Error < StandardError; end
|
|
30
|
+
class TimeoutError < Error; end
|
|
31
|
+
class HttpError < Error; end
|
|
32
|
+
|
|
33
|
+
# ============ 核心 GET 方法 ============
|
|
34
|
+
#
|
|
35
|
+
# @param url [String] 完整 URL 或路径;若已含 query,params 会追加在其后
|
|
36
|
+
# @param params [Hash] 查询参数,自动用 URI.encode_www_form 编码到 query string
|
|
37
|
+
# @param headers [Hash] 自定义请求头(自动覆盖默认头)
|
|
38
|
+
# @param opts [Hash] 选项: :timeout, :open_timeout, :max_retries
|
|
39
|
+
#
|
|
40
|
+
# @return [Hash/String] 解析后的 JSON 或原始响应体
|
|
41
|
+
#
|
|
42
|
+
def self.get(url, params = {}, headers = {}, opts = {})
|
|
43
|
+
uri = URI.parse(url)
|
|
44
|
+
unless params.empty?
|
|
45
|
+
extra = URI.encode_www_form(params)
|
|
46
|
+
uri.query = uri.query ? "#{uri.query}&#{extra}" : extra
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
req = Net::HTTP::Get.new(uri)
|
|
50
|
+
req['User-Agent'] = headers['User-Agent'] || 'ruby-http-getter/1.0'
|
|
51
|
+
req['Accept'] = headers['Accept'] || 'application/json'
|
|
52
|
+
|
|
53
|
+
# 认证头(支持 Bearer Token)
|
|
54
|
+
if headers['Authorization'] || headers[:authorization]
|
|
55
|
+
req['Authorization'] = headers['Authorization'] || headers[:authorization]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# 追加其余自定义头
|
|
59
|
+
headers.each do |k, v|
|
|
60
|
+
next if %w[Accept Authorization User-Agent].include?(k.to_s)
|
|
61
|
+
req[k.to_s] = v.to_s
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
execute(uri, req, opts)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# ============ 快捷方法 ============
|
|
68
|
+
|
|
69
|
+
# GET JSON(API 调用最常用)
|
|
70
|
+
# 自动设置 Accept: application/json,期望服务端返回 JSON
|
|
71
|
+
def self.get_json(url, params = {}, headers = {}, opts = {})
|
|
72
|
+
headers = headers.merge('Accept' => 'application/json')
|
|
73
|
+
get(url, params, headers, opts)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# GET 纯文本(原始响应体,不做 JSON 解析)
|
|
77
|
+
# 自动设置 Accept: text/plain,返回原始 String
|
|
78
|
+
def self.get_text(url, params = {}, headers = {}, opts = {})
|
|
79
|
+
headers = headers.merge('Accept' => 'text/plain')
|
|
80
|
+
opts = opts.merge(__raw__: true)
|
|
81
|
+
get(url, params, headers, opts)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def self.execute(uri, request, opts = {})
|
|
87
|
+
timeout = opts[:timeout] || 60
|
|
88
|
+
open_timeout = opts[:open_timeout] || 10
|
|
89
|
+
max_retries = opts[:max_retries] || 2
|
|
90
|
+
raw = opts[:__raw__] == true
|
|
91
|
+
|
|
92
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
93
|
+
http.use_ssl = uri.scheme == 'https'
|
|
94
|
+
http.open_timeout = open_timeout
|
|
95
|
+
http.read_timeout = timeout
|
|
96
|
+
|
|
97
|
+
retries = 0
|
|
98
|
+
begin
|
|
99
|
+
response = http.request(request)
|
|
100
|
+
parse(response, raw)
|
|
101
|
+
rescue Net::OpenTimeout, Net::ReadTimeout => e
|
|
102
|
+
retries += 1
|
|
103
|
+
if retries <= max_retries
|
|
104
|
+
sleep(2 ** retries)
|
|
105
|
+
retry
|
|
106
|
+
end
|
|
107
|
+
raise TimeoutError, "请求超时(已重试 #{max_retries} 次): #{e.message}"
|
|
108
|
+
ensure
|
|
109
|
+
http.finish if http.started?
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def self.parse(response, raw = false)
|
|
114
|
+
body = response.body.to_s
|
|
115
|
+
code = response.code.to_i
|
|
116
|
+
|
|
117
|
+
if code >= 400
|
|
118
|
+
begin
|
|
119
|
+
err = JSON.parse(body)
|
|
120
|
+
msg = err.dig('error', 'message') || body
|
|
121
|
+
rescue
|
|
122
|
+
msg = body
|
|
123
|
+
end
|
|
124
|
+
raise HttpError, "HTTP #{code}: #{msg}"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
return body if raw
|
|
128
|
+
|
|
129
|
+
JSON.parse(body) rescue body
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# ============ 使用示例 ============
|
|
134
|
+
if __FILE__ == $0
|
|
135
|
+
# 示例 1: GET JSON(调用公开 API)
|
|
136
|
+
begin
|
|
137
|
+
res = HttpGetter.get_json(
|
|
138
|
+
'https://httpbin.org/get',
|
|
139
|
+
{ page: 1, size: 20 },
|
|
140
|
+
{ 'Authorization' => 'Bearer your-token-here' },
|
|
141
|
+
{ timeout: 30 }
|
|
142
|
+
)
|
|
143
|
+
puts res['args']
|
|
144
|
+
rescue HttpGetter::HttpError => e
|
|
145
|
+
puts "请求失败: #{e.message}"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# 示例 2: GET 纯文本
|
|
149
|
+
# res = HttpGetter.get_text('https://httpbin.org/robots.txt')
|
|
150
|
+
# puts res # => 原始字符串
|
|
151
|
+
|
|
152
|
+
# 示例 3: 通用 GET(自定义头,url 已含 query,params 追加)
|
|
153
|
+
# res = HttpGetter.get(
|
|
154
|
+
# 'https://httpbin.org/get?foo=1',
|
|
155
|
+
# { bar: '2' },
|
|
156
|
+
# { 'X-Request-ID' => 'uuid-5678' }
|
|
157
|
+
# )
|
|
158
|
+
# puts res['args'] # => {"foo"=>"1", "bar"=>"2"}
|
|
159
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../transport'
|
|
4
|
+
|
|
5
|
+
module IceJade
|
|
6
|
+
module Getter
|
|
7
|
+
# 通用 HTTP GET 客户端
|
|
8
|
+
#
|
|
9
|
+
# 与 IceJade::Poster::Client 并列,专用负责 GET 请求。
|
|
10
|
+
# 底层传输能力(URI 拼接、请求执行、响应适配、超时重试)复用 IceJade::Transport,
|
|
11
|
+
# 与 Poster 仅 HTTP 动词与参数构造不同:GET 幂等,无请求体,参数走 query string。
|
|
12
|
+
#
|
|
13
|
+
# 零外部依赖,内置超时控制、指数退避重试、统一 Response 包装,
|
|
14
|
+
# 与 Quantum 模块风格一致,错误不抛异常。
|
|
15
|
+
#
|
|
16
|
+
# @example 基础用法
|
|
17
|
+
# getter = IceJade::Getter::Client.new(
|
|
18
|
+
# base_url: 'https://api.example.com',
|
|
19
|
+
# headers: { 'Authorization' => 'Bearer token' },
|
|
20
|
+
# timeout: 30
|
|
21
|
+
# )
|
|
22
|
+
# resp = getter.get('/users', params: { page: 1, size: 20 })
|
|
23
|
+
# puts resp.data if resp.success?
|
|
24
|
+
#
|
|
25
|
+
class Client
|
|
26
|
+
include IceJade::Transport
|
|
27
|
+
|
|
28
|
+
# @param base_url [String, nil] 基础 URL,后续请求可传相对路径
|
|
29
|
+
# @param headers [Hash] 默认请求头(每次请求自动合并,同名会被覆盖)
|
|
30
|
+
# @param timeout [Integer] 读取超时(秒),默认 60
|
|
31
|
+
# @param open_timeout [Integer] 连接超时(秒),默认 10
|
|
32
|
+
# @param max_retries [Integer] 超时后最大重试次数,默认 2
|
|
33
|
+
def initialize(base_url: nil, headers: {}, timeout: 60, open_timeout: 10, max_retries: 2)
|
|
34
|
+
init_transport(base_url: base_url, headers: headers,
|
|
35
|
+
timeout: timeout, open_timeout: open_timeout,
|
|
36
|
+
max_retries: max_retries)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# ------------------------------------------------------------------
|
|
40
|
+
# 通用 GET
|
|
41
|
+
# ------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
# 发送通用 GET 请求
|
|
44
|
+
#
|
|
45
|
+
# 与 Poster 的 post 系列方法对称:返回 IceJade::Response,错误不抛异常。
|
|
46
|
+
# GET 幂等,超时重试同样安全。
|
|
47
|
+
#
|
|
48
|
+
# @param url [String] 完整 URL 或相对路径(需先设置 base_url)
|
|
49
|
+
# @param params [Hash, nil] 查询参数,自动用 URI.encode_www_form 编码到 query string;
|
|
50
|
+
# 若 url 已含 query,则在其后追加
|
|
51
|
+
# @param headers [Hash] 额外请求头(与默认头合并,同名覆盖)
|
|
52
|
+
# @return [IceJade::Response]
|
|
53
|
+
#
|
|
54
|
+
# @example 带查询参数
|
|
55
|
+
# resp = getter.get('/users', params: { page: 1, size: 20 })
|
|
56
|
+
# puts resp.data if resp.success?
|
|
57
|
+
#
|
|
58
|
+
# @example 自定义请求头
|
|
59
|
+
# resp = getter.get('/headers', headers: { 'X-Request-ID' => 'uuid-5678' })
|
|
60
|
+
#
|
|
61
|
+
# @example 错误响应
|
|
62
|
+
# resp = getter.get('/status/404')
|
|
63
|
+
# resp.success? # => false
|
|
64
|
+
# resp.code # => 404
|
|
65
|
+
def get(url, params: nil, headers: {})
|
|
66
|
+
uri = build_uri(url)
|
|
67
|
+
if params && !params.empty?
|
|
68
|
+
extra = URI.encode_www_form(params)
|
|
69
|
+
uri.query = uri.query ? "#{uri.query}&#{extra}" : extra
|
|
70
|
+
end
|
|
71
|
+
merged_headers = merge_headers(default_headers, normalize_headers(headers))
|
|
72
|
+
req = Net::HTTP::Get.new(uri)
|
|
73
|
+
apply_headers(req, merged_headers)
|
|
74
|
+
execute(uri, req)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|