rubyのhttpclientでhttpsからhttpにredirectするとエラーになる

  • このエントリーをはてなブックマークに追加

httpclientでredirectの自動追従は次のオプションで出来る。

client = HTTPClient.new()
client.get('https://nghttp2.org/httpbin/redirect/2', :follow_redirect => true)

httpbinのredirectが404になるので、代わりのサイトで検証。

しかし、httpsからhttpへのredirectはBadResponseErrorになります。

client = HTTPClient.new()
client.get('https://nghttp2.org/httpbin/redirect-to?url=http%3A%2F%2Fexample.com', :follow_redirect => true)

ライブラリのコードはこんな感じでif https?(uri) && !https?(newuri)このコードで弾かれています。

def default_redirect_uri_callback(uri, res)
newuri = urify(res.header['location'][0])
if !http?(newuri) && !https?(newuri)
warn("#{newuri}: a relative URI in location header which is not recommended")
warn("'The field value consists of a single absolute URI' in HTTP spec")
newuri = uri + newuri
end
if https?(uri) && !https?(newuri)
raise BadResponseError.new("redirecting to non-https resource")
end
puts "redirect to: #{newuri}" if $DEBUG
newuri
end
https://github.com/nahi/httpclient/blob/master/lib/httpclient.rb#L722-L734

HTTPClient::BadResponseError (redirecting to non-https resource)

これはこれで正しいのですが、クローラーを書くときは無視したいことが多いです。そんな時は、clientのredirect_uri_callbackを自前のロジックに変更します。

client = HTTPClient.new()
client.redirect_uri_callback = lambda {|uri, res|
newuri = HTTPClient::Util.urify(res.header['location'][0])
if (newuri.scheme && (newuri.scheme.downcase == "https" || newuri.scheme.downcase == "http"))
else
newuri = uri + newuri
end
newuri
}

参考情報

rubyのhttpclientでgzip対応

  • このエントリーをはてなブックマークに追加

httpclientでgzipのresponseを自動で処理するためのオプション。

client = HTTPClient.new()
client.transparent_gzip_decompression = true

参考情報

PlantUML-ServerをBasic認証付きで公開する

  • このエントリーをはてなブックマークに追加

plantuml-serverはdocker imageが既にあるので簡単に公開する事ができます。
外部サーバに立てると、誰でもアクセスできてしまうため、Basic認証を付けたいことがあります。WebのUIから使うだけならIDP連携でOAuth認証を入れてしまえば良いのですが、Visual Studio Codeのプラグインと連携させる場合は、OAuthは対応していないため、Basic認証にするしかありません。

docker-composeを利用して、nginxでbasic認証をかけてplantumlにproxyするのが簡単です。
そして、nginxのbasic認証に特化したイメージがあります ⇒ quay.io/dtan4/nginx-basic-auth-proxy

しかし、普通に設定すると、PlantUMLの画面でホスト名がplantuml:8080というホスト名になってしまいます。
これは、例えば、http://example.comにデプロイしたとして、Userhttp://example.comnginxhttp://plantuml:8080/PlantUMLのApplicationサーバ(Jetty or Tomcat) となり、PlantUML(Jetty)から見ると、plantuml:8080というホスト名でアクセスに来ているからです。

PlantUMLのコード

PlantUMLのコードでは、JSPのTagLibで次のように記述されています。

<c:set var="contextroot" value="${pageContext.request.contextPath}" />
<c:if test="${(pageContext.request.scheme == 'http' && pageContext.request.serverPort != 80) ||
(pageContext.request.scheme == 'https' && pageContext.request.serverPort != 443) }">
<c:set var="port" value=":${pageContext.request.serverPort}" />
</c:if>
<c:set var="scheme" value="${(not empty header['x-forwarded-proto']) ? header['x-forwarded-proto'] : pageContext.request.scheme}" />
<c:set var="hostpath" value="${scheme}://${pageContext.request.serverName}${port}${contextroot}" />
<c:if test="${!empty encoded}">
<c:set var="imgurl" value="${hostpath}/png/${encoded}" />
<c:set var="svgurl" value="${hostpath}/svg/${encoded}" />
<c:set var="txturl" value="${hostpath}/txt/${encoded}" />
<c:if test="${!empty mapneeded}">
<c:set var="mapurl" value="${hostpath}/map/${encoded}" />
</c:if>
</c:if>

<a href="${svgurl}">View as SVG</a>

pageContext.request.serverNameplantuml:8080になります。
一方、nginx-basic-auth-proxyでは、X-Forwarded-Hostを設定しています。(参考)

つまり、Jetty側でこのヘッダをHostとして扱えば良いわけです。
Jettyでは、X-Forwarded-xxの処理を良い感じに処理してくれるモジュールとしてhttp-forwardedがあります。

PlantUMLのJetty版の元のイメージはこちらを利用しており、
http-forwardedはデフォルトでは有効になっていませんが、/usr/local/jetty/etc/jetty-http-forwarded.xmlにあります。
そして、有効にするためには、--module=http-forwardedの引数を付ければ有効になります。

PlantUMLのDockerfileの定義は、Entrypointが/docker-entrypoint.sh。Commandがjava -jar /usr/local/jetty/start.jarです。
ここに引数を追加するには、command: java -jar /usr/local/jetty/start.jar --module=http-forwardedとすれば良いです。

docker-compose.yaml

こんな感じで。
ALLOW_PLANTUML_INCLUDEは、!includeで綺麗にするスタイルを外部ファイル化しており、毎回当てるので有効にしています。

version: "3.3"
services:
nginx-proxy:
image: quay.io/dtan4/nginx-basic-auth-proxy
ports:
- 80:80
environment:
BASIC_AUTH_USERNAME: test-user
BASIC_AUTH_PASSWORD: test-password
PROXY_PASS: http://plantuml:8080/
SERVER_NAME: proxy.internal
networks:
- container-link
plantuml:
image: plantuml/plantuml-server:jetty
command:
- "java"
- "-jar"
- "/usr/local/jetty/start.jar"
- "--module=http-forwarded"
environment:
- ALLOW_PLANTUML_INCLUDE=true
networks:
- container-link
networks:
container-link:

VS Codeのプラグインの設定

利用しているPlantUMLのPluginはこちら。

名前: PlantUML
ID: jebbs.plantuml
説明: Rich PlantUML support for Visual Studio Code.
バージョン: 2.14.5
パブリッシャー: jebbs
VS Marketplace リンク: https://marketplace.visualstudio.com/items?itemName=jebbs.plantuml

次のように、RenderをLocalからPlantUMLServerにします。
そして、Serverにdocker-composeで起動したURLを設定します。Basic認証の情報はURLに入れる事ができるので、http://test-user:test-password@example.com/のように設定します。

参考

Application Guardの設定をPowerShellで取得する

  • このエントリーをはてなブックマークに追加

Azureで管理されているポリシーが各端末に適用されるわけですが、そのポリシーはレジストリに格納されています。

  • キー: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\PolicyManager\current\device\NetworkIsolation
  • エントリ: EnterpriseCloudResources

もしくは、上記キーの EnterpriseCloudResources_WinningProviderの値 ⇒ ① として、以下のキーの場所。
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\PolicyManager\Providers\①\default\Device\NetworkIsolation

プロビジョニングされた内容は、以下のキー配下に履歴として残っていきます。

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Provisioning\NodeCache\CSP\Device\MS DM Server\Nodes\

取得方法

(Get-ItemProperty -LiteralPath HKLM:\SOFTWARE\Microsoft\PolicyManager\current\device\NetworkIsolation).EnterpriseCloudResources | %{ $_ -replace '\|', "`n" }

|で区切られているので、改行に変換します。
PowerShellでは改行に変換する時、バッククォートなんですね。

参考

Safari Booksに日本語の本がたくさん追加されていた(計112冊)

  • このエントリーをはてなブックマークに追加

先月まで58冊だったのですが、今日見たら112冊になってました。
追加日を見ると、2021年5月19日~24日にかけて追加されたようです。

デザインの伝え方Javaパフォーマンス をはじめ、よさげな本がたくさんあります!

安く読む方法は、ACMの会員になるのが良いです。以下のリンクを参照。

ACM会員になってオライリーの本が読み放題

日本語書籍の一覧

カバー タイトル 出版日/追加日
入門 ソーシャルデータ 第2版 ―ソーシャルウェブのデータマイニング 2014/06/20
2021/05/24
Cクイックリファレンス 第2版 2016/11/04
2021/05/24
JavaScriptで学ぶ関数型プログラミング 2014/01/17
2021/05/20
マイクロインタラクション ―UI/UXデザインの神が宿る細部 2014/03/18
2021/05/19
AngularJSアプリケーション開発ガイド 2014/04/17
2021/05/19
実践 Vagrant 2014/02/20
2021/05/19
Effective Modern C++ ―C++11/14プログラムを進化させる42項目 2015/09/17
2021/05/19
Backbone.jsアプリケーション開発ガイド 2014/02/06
2021/05/19
統計クイックリファレンス 第2版 2015/01/23
2021/05/19
戦略的データサイエンス入門 ―ビジネスに活かすコンセプトとテクニック 2014/07/18
2021/05/19
Rクイックリファレンス 第2版 2014/01/24
2021/05/19
Bluetooth Low Energyをはじめよう 2015/02/24
2021/05/19
エンジニアのためのフィードバック制御入門 2014/07/25
2021/05/19
リーン顧客開発 ―「売れないリスク」を極小化する技術 2015/04/24
2021/05/19
Think Stats 第2版 ―プログラマのための統計入門 2015/08/18
2021/05/19
Rパッケージ開発入門 ―テスト、文書化、コード共有の手法を学ぶ 2016/02/04
2021/05/19
Think Bayes ―プログラマのためのベイズ統計入門 2014/09/05
2021/05/19
デザイニング・マルチデバイス・エクスペリエンス ―デバイスの枠を超えるUXデザインの探求 2014/12/24
2021/05/19
CSSシークレット ―47のテクニックでCSSを自在に操る 2016/07/22
2021/05/19
Sparkによる実践データ解析 ―大規模データのための機械学習事例集 2016/01/22
2021/05/19
Cython ―Cとの融合によるPythonの高速化 2015/06/18
2021/05/19
ユーザーストーリーマッピング 2015/07/24
2021/05/19
インタラクティブ・データビジュアライゼーション ― D3.jsによるデータの可視化 2014/02/18
2021/05/19
データ分析によるネットワークセキュリティ 2016/06/10
2021/05/19
リーンブランディング ―リーンスタートアップによるブランド構築 2016/08/25
2021/05/19
情報アーキテクチャ 第4版 ―見つけやすく理解しやすい情報設計 2016/11/17
2021/05/19
Docker 2016/08/12
2021/05/19
アルゴリズムクイックリファレンス 第2版 2016/12/22
2021/05/19
初めてのSpark 2015/08/21
2021/05/19
デザインスプリント ―プロダクトを成功に導く短期集中実践ガイド 2016/11/25
2021/05/19
初めてのAnsible 2016/04/15
2021/05/19
コマンドラインではじめるデータサイエンス ―分析プロセスを自在に進めるテクニック 2015/09/15
2021/05/19
実践 Android Developer Tools 2014/04/22
2021/05/19
アンダースタンディング コンピュテーション ―単純な機械から不可能なプログラムまで 2014/09/17
2021/05/19
データ匿名化手法 ―ヘルスデータ事例に学ぶ個人情報保護 2015/05/22
2021/05/19
詳説 イーサネット 第2版 2015/06/09
2021/05/19
アジャイルデータサイエンス ―スケーラブルに構築するビッグデータアプリケーション 2014/04/24
2021/05/19
ハイパフォーマンス ブラウザネットワーキング ―ネットワークアプリケーションのためのパフォーマンス最適化 2014/05/15
2021/05/19
ハイパフォーマンスPython 2015/11/19
2021/05/19
詳解 iOS SDK 第4版 ―ワンランク上のiPhone/iPadプログラミング 2014/12/18
2021/05/19
Javaパフォーマンス 2015/04/10
2021/05/19
パフォーマンス向上のためのデザイン設計 2016/06/24
2021/05/19
モバイルデザインパターン 第2版 ―ユーザーインタフェースのためのパターン集 2015/02/10
2021/05/19
データサイエンス講義 2014/10/24
2021/05/19
RStudioではじめるRプログラミング入門 2015/03/24
2021/05/19
Lean Analytics ―スタートアップのためのデータ解析と活用法 2015/01/23
2021/05/19
デザインの伝え方 ―組織の合意を得るコミュニケーション術 2016/09/15
2021/05/19
入門 iOS SDK ―初めてのiPhone/iPadプログラミング 2014/06/20
2021/05/19
グラフデータベース ―Neo4jによるグラフデータモデルとグラフデータベース入門 2015/03/24
2021/05/19
リーンエンタープライズ ―イノベーションを実現する創発的な組織づくり 2016/10/14
2021/05/19
Haskellによる並列・並行プログラミング 2014/08/20
2021/05/19
UX戦略 ―ユーザー体験から考えるプロダクト作り 2016/05/24
2021/05/19
マイクロサービスアーキテクチャ 2016/02/25
2021/05/19
ZooKeeperによる分散システム管理 2014/10/07
2021/05/19
エレガントなSciPy ―Pythonによる科学技術計算 2018/11/09
2020/12/14
デザイニング・ボイスユーザーインターフェース ―音声で対話するサービスのためのデザイン原則 2018/11/30
2020/12/14
Infrastructure as Code ―クラウドにおけるサーバ管理の原則とプラクティス 2017/03/17
2020/12/14
初めてのPerl 第7版 2018/01/19
2020/12/14
Rによるテキストマイニング ―tidytextを活用したデータ分析と可視化の基礎 2018/05/17
2020/12/14
PythonとJavaScriptではじめるデータビジュアライゼーション 2017/08/24
2020/12/14
Rグラフィックスクックブック 第2版 ―ggplot2によるグラフ作成のレシピ集 2019/11/19
2020/12/14
ゼロトラストネットワーク ―境界防御の限界を超えるためのセキュアなシステム設計 2019/10/25
2020/12/14
Pythonデータサイエンスハンドブック ―Jupyter、NumPy、pandas、Matplotlib、scikit-learnを使ったデータ分析、機械学習 2018/05/25
2020/12/14
プログラミングRust 2018/08/09
2020/12/14
初めてのPHP 2017/03/17
2020/12/14
Rではじめるデータサイエンス 2017/10/24
2020/12/14
Lean UX 第2版 ―アジャイルなチームによるプロダクト開発 2017/07/03
2020/12/14
実践 CSIRTプレイブック ―セキュリティ監視とインシデント対応の基本計画 2018/05/18
2020/12/14
Head First Python 第2版 ―頭とからだで覚えるPythonの基本 2018/03/23
2020/12/14
Pythonによるデータ分析入門 第2版 ―NumPy、pandasを使ったデータ処理 2018/07/25
2020/12/14
GitHubツールビルディング ―GitHub APIを活用したワークフローの拡張とカスタマイズ 2017/05/09
2020/12/14
バイオビルダー ―合成生物学をはじめよう 2018/11/20
2020/12/14
Optimized C++ ―最適化、高速化のためのプログラミングテクニック 2017/02/21
2020/12/10
入門 監視 ―モダンなモニタリングのためのデザインパターン 2019/01/16
2020/11/19
Go言語による並行処理 2018/10/25
2020/11/19
Fluent Python ―Pythonicな思考とコーディング手法 2017/10/06
2020/11/19
アイソモーフィックJavaScript 2017/07/03
2020/11/19
ベタープログラマ ―優れたプログラマになるための38の考え方とテクニック 2017/12/14
2020/11/19
入門 Kubernetes 2018/03/20
2020/11/19
Pythonではじめる機械学習 ―scikit-learnで学ぶ特徴量エンジニアリングと機械学習の基礎 2017/05/24
2020/11/19
機械学習のための特徴量エンジニアリング ―その原理とPythonによる実践 2019/02/21
2020/11/19
エンジニアのためのマネジメントキャリアパス ―テックリードからCTOまでマネジメントスキル向上ガイド 2018/09/25
2020/11/19
SVGエッセンシャルズ 第2版 2017/05/16
2020/11/19
実践 Deep Learning ―PythonとTensorFlowで学ぶ次世代の機械学習アルゴリズム 2018/04/25
2020/11/19
進化的アーキテクチャ ―絶え間ない変化を支える 2018/08/17
2020/11/19
Reactビギナーズガイド ―コンポーネントベースのフロントエンド開発入門 2017/03/10
2020/11/19
初めてのJavaScript 第3版 ―ES2015以降の最新ウェブ開発 2017/01/19
2020/11/19
プロダクションレディマイクロサービス ―運用に強い本番対応システムの実装と標準化 2017/09/12
2020/11/19
Effective DevOps ―4本柱による持続可能な組織文化の育て方 2018/03/23
2020/11/19
PythonによるWebスクレイピング 第2版 2019/03/25
2020/11/19
分散システムデザインパターン ―コンテナを使ったスケーラブルなサービスの設計 2019/04/19
2020/11/19
データ指向アプリケーションデザイン ―信頼性、拡張性、保守性の高い分散システム設計の原理 2019/07/17
2020/11/19
詳説 Deep Learning ―実務者のためのアプローチ 2019/08/08
2020/11/19
入門 Prometheus ―インフラとアプリケーションのパフォーマンスモニタリング 2019/05/17
2020/11/19
初めてのGraphQL ―Webサービスを作って学ぶ新世代API 2019/11/11
2020/11/19
SRE サイトリライアビリティエンジニアリング ―Googleの信頼性を支えるエンジニアリングチーム 2017/08/10
2020/11/19
プログラミングROS ―Pythonによるロボットアプリケーション開発 2017/12/12
2020/11/19
詳解 OpenCV 3 ―コンピュータビジョンライブラリを使った画像処理・認識 2018/05/25
2020/11/19
Unityによるモバイルゲーム開発 ―作りながら学ぶ2D/3Dゲームプログラミング入門 2018/08/23
2020/11/19
マスタリング・イーサリアム ―スマートコントラクトとDAppの構築 2019/11/28
2020/11/16
インテリジェンス駆動型インシデントレスポンス ―攻撃者を出し抜くサイバー脅威インテリジェンスの実践的活用法 2018/12/25
2020/11/16
Python機械学習クックブック 2018/12/14
2020/11/16
ITIL Service Design (Japanese Translation) 2013/06/01
2020/10/29
ITIL Service Operation (Japanese Translation) 2013/06/01
2020/10/29
ITIL Service Strategy (Japanese Translation) 2013/06/01
2020/10/29
ITIL Service Transition (Japanese Translation) 2013/06/01
2020/10/29
ITIL Practitioner Guidance (Japanese Translation) 2017/03/01
2020/10/29
ITIL Continual Service Improvement (Japanese Translation) 2013/06/01
2020/10/29
Feedback That Works: How to Build and Deliver Your Message (Japanese) 2008/08/01
2020/10/26
Developmental Assignments: Creating Learning Experiences Without Changing Jobs (Japanese) 2008/07/31
2020/10/26
ITIL Foundation Handbook (Japanese Translation) 2015/06/01
2020/10/23
ITIL Foundation, ITIL 4 edition (Japanese Translation) 2019/11/01
2020/10/23

Jacksonである階層の属性を別の階層のObjectにマッピングする

  • このエントリーをはてなブックマークに追加

以下のJsonがあるとして、cursorhas_nextはPaginationに関するものだから、別のObjectにマッピングしたい。

{
"id": "abc",
"cursor": "next-cursor",
"has_next": true
}

普通にMappingするとこういうクラスを用意するわけだけど、

@Data
public static class MyRoot {
private String id;
private String cursor;
@JsonProperty("has_next") private boolean hasNext;
}

こういう感じの構造にマッピングしたい。

@Data
public static class MyRoot {
private String id;
private Paging paging;
}
@Data
public static class Paging {
@JsonProperty("cursor") private String cursor;
@JsonProperty("has_next") private boolean hasNext;
}

方法: @JsonUnwrappedを使う

@Data
public static class MyRoot {
private String id;
@JsonUnwrapped
private Paging paging;
}
@Data
public static class Paging {
@JsonProperty("cursor")
private String cursor;
@JsonProperty("has_next")
private boolean hasNext;
}

@Test
public static void main(String[] args) throws Exception {
String jsonString = """
{
"id": "abc",
"cursor": "next-cursor",
"has_next": true
}
""".indent(0);

ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
MyRoot root = mapper.readValue(jsonString, MyRoot.class);
System.out.println(root);
JacksonTest.MyRoot(id=abc, paging=JacksonTest.Paging(cursor=next-cursor, hasNext=true))

もし、MyRootクラスと@JsonUnwrappedされたPagingクラスに同じ属性名が存在する場合、MyRoot側に値が設定されます。

Twitter gemでrate limitのAPIをコールしたい

  • このエントリーをはてなブックマークに追加

メソッドがないので、Requestを愚直に作ってレスポンスを取得する。

require "twitter"

client = Twitter::REST::Client.new do |config|
config.consumer_key = "***"
config.consumer_secret = "***"
config.access_token = "***"
config.access_token_secret = "***"
end

rate_limits = Twitter::REST::Request.new(client, :get, '/1.1/application/rate_limit_status.json').perform
rate_limits[:resources].each_pair do |category, hash|
hash.each_pair do |api_name, context|
puts [
category,
api_name,
context[:limit],
context[:remaining],
Time.at(context[:reset]),
].join("\t")
end
end

sinatra + omniauth-twitterでForbiddenになる

  • このエントリーをはてなブックマークに追加

インターネットを適当に検索して出てくるサンプルを動かしてみると、CVE-2015-9284対応前のバージョンのサンプルばかりで、/auth/twitterへアクセス時にForbiddenエラーが発生してしまう。

Gemのバージョン
gem "sinatra"           , "2.1.0"
gem "sinatra-contrib" , "2.1.0"
gem "omniauth" , "2.0.4"
gem "omniauth-oauth" , "1.2.0"
gem "omniauth-twitter" , "1.4.0"

マニュアルによると、

By default, this uses rack-protection’s AuthenticityToken class to validate authenticity tokens. If you are using a rack based framework like sinatra, you can find an example of how to add authenticity tokens to your view here.

https://github.com/omniauth/omniauth/wiki/Upgrading-to-2.0#racksinatra

デフォルトではAuthenticityToken クラスを使用して認証トークンを検証します。
sinatra のようなrackベースのフレームワークを使用している場合の、認証トークンをviewに追加するサンプルがここにあります。

と記載があり、
/auth/:providerのrequest parameterとしてauthenticity_tokenが必要とのこと。
値はrequest.env["rack.session"]["csrf"]を。

こんな感じのが必要.

<form method="post" action="/auth/twitter">
<input type="hidden" name="authenticity_token" value="<%=request.env['rack.session']['csrf']%>">
<button type="submit">login</button>
</form>

参考情報

omniauth-twitterで/auth/twitterが有効にならない

  • このエントリーをはてなブックマークに追加

サンプルで遊んでいたら、/auth/:providerが有効にならなくて、なんでだろうなと。
CSRFの対策で、/auth/:providerはgetメソッドでは受け付けません。

p OmniAuth.config.allowed_request_methods
> [:post]

https://github.com/omniauth/omniauth/pull/1010で変更が入り、omniauth 2.0以降に取り込まれています。

以下のようなことはしてはいけません。
ちゃんとpostでコールしましょうね。

OmniAuth.config.allowed_request_methods = [:get, :post]

参考情報

Concurrent::Promises.zipとFuture.delay

  • このエントリーをはてなブックマークに追加

concurrent-rubyの使い方を調べていて、こちらのページを見つけたのですが、解説が正しくなかったので、正しい挙動をメモ。

このQiitaのページでは、以下のように記載があります。

zipとういうメソッドが用意されているのでそれを使用すると、同時にスレッドが実行され、それらの返り値が配列にまとめられます。

しかし、Concurrent::Promises.futurefuture_onのshortcutであり、future_oneval on immediatelyであるので、futureを作った時点で実行が始まります。そのため、zipとは関係なく、threadの状態はfullfilledになります。

Constructs new Future which will be resolved after block is evaluated on default executor. Evaluation begins immediately.

zipメソッドで処理を発火したい場合は、delayを使います。

sample program
threads = (1..5).map do |i|
Concurrent::Promises.delay(i) do |i|
n = rand(5) + 1
puts "[#{i}] start: #{Time.now} / sleep=#{n}"
sleep n
puts "[#{i}] finished: #{Time.now}"
"result: #{i}"
end
end

sleep 3
puts "before zip: #{Time.now}"
Concurrent::Promises.zip(*threads)
.then{|*hashes| hashes.reduce(&:merge) }
.value

実行結果(delay)

result by delay
before zip: 2021-04-11 14:56:35 +0900            # zipで発火するまでスレッドは実行されていない
[5] start: 2021-04-11 14:56:35 +0900 / sleep=3
[4] start: 2021-04-11 14:56:35 +0900 / sleep=3
[3] start: 2021-04-11 14:56:35 +0900 / sleep=4
[2] start: 2021-04-11 14:56:35 +0900 / sleep=5
[1] start: 2021-04-11 14:56:35 +0900 / sleep=4
[4] finished: 2021-04-11 14:56:38 +0900
[5] finished: 2021-04-11 14:56:38 +0900
[1] finished: 2021-04-11 14:56:39 +0900
[3] finished: 2021-04-11 14:56:39 +0900
[2] finished: 2021-04-11 14:56:40 +0900
```

実行結果(futureの場合)

result by future
ruby aaa.rb 
[1] start: 2021-04-11 14:57:14 +0900 / sleep=4
[2] start: 2021-04-11 14:57:14 +0900 / sleep=5
[3] start: 2021-04-11 14:57:14 +0900 / sleep=4
[4] start: 2021-04-11 14:57:14 +0900 / sleep=1
[5] start: 2021-04-11 14:57:14 +0900 / sleep=1
[5] finished: 2021-04-11 14:57:15 +0900
[4] finished: 2021-04-11 14:57:15 +0900
before zip: 2021-04-11 14:57:17 +0900 # ▲zipの前にスレッド実行されている
[3] finished: 2021-04-11 14:57:18 +0900
[1] finished: 2021-04-11 14:57:18 +0900
[2] finished: 2021-04-11 14:57:19 +0900

参考情報