こんにちは、hachi8833です。
🔗Rails: 先週の改修(Rails公式ニュースより)
上より新しい公式情報を先回りしてチェックしたものもあります。
🔗 Active Storageのダウンロード中断・再開でRangeヘッダーが不正な場合のエラーを'416 Range Not Satisfiable'に変更
再現手順
require 'bundler/inline'
gemfile(true) do
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
# Activate the gem you are reporting the issue against.
gem 'rails', '~> 7.0.0'
gem 'sqlite3'
end
require 'active_record/railtie'
require 'active_storage/engine'
require 'tmpdir'
class TestApp < Rails::Application
config.root = __dir__
config.eager_load = false
config.session_store :cookie_store, key: 'cookie_store_key'
secrets.secret_key_base = 'secret_key_base'
config.hosts << 'www.example.com'
config.logger = Logger.new($stdout)
Rails.logger = config.logger
config.active_storage.service = :local
config.active_storage.service_configurations = {
local: {
root: Dir.tmpdir,
service: 'Disk'
}
}
end
ENV['DATABASE_URL'] = 'sqlite3::memory:'
Rails.application.initialize!
require ActiveStorage::Engine.root.join('db/migrate/20170806125915_create_active_storage_tables.rb').to_s
ActiveRecord::Schema.define do
CreateActiveStorageTables.new.change
create_table :users, force: true
end
class User < ActiveRecord::Base
has_one_attached :pdf
end
require 'minitest/autorun'
# Download a file with incorrect partial byte range
class PartialDownloadTest < ActionDispatch::IntegrationTest
test 'Partial download' do
user = User.create!(
pdf: {
content_type: 'application/pdf',
filename: 'dummy.pdf',
io: ::StringIO.new(SecureRandom.random_bytes(1000))
}
)
# Get URL for download
get "/#{Rails.application.routes.url_helpers.rails_blob_path(user.pdf, only_path: true)}"
assert_response :redirect
# Download with non-existing range
get(response.location, headers: { 'Range' => 'bytes=10000-10000' })
# Should respond with status 416 Range not satisfiable, but throws a RoutingError and responds with 404 not found
assert_response 416
end
end
期待される振る舞い
存在しないファイルの一部をブラウザがリクエストしたとき(Safariで起きがち)のレスポンスは416 Range not satisfiableになるべき。
実際の振る舞い
レスポンスが404 Not foundになり、ActionController::RoutingError
が発生する。
システム環境
Rails: 7.0.3.1
Ruby: 3.0.2p107
issue #45890より
参考: §5.6 X-Sendfileヘッダー -- アセットパイプライン - Railsガイド
つっつきボイス:「X-Cascadeは、Rackが追加して解釈するヘッダーなのか↓」
# activestorage/app/controllers/concerns/active_storage/file_server.rb#L3
+require "active_support/core_ext/hash/except"
+
module ActiveStorage::FileServer # :nodoc:
private
def serve_file(path, content_type:, disposition:)
Rack::File.new(nil).serving(request, path).tap do |(status, headers, body)|
self.status = status
self.response_body = body
headers.each do |name, value|
response.headers[name] = value
end
+ response.headers.except!("X-Cascade", "x-cascade") if status == 416
response.headers["Content-Type"] = content_type || DEFAULT_SEND_FILE_TYPE
response.headers["Content-Disposition"] = disposition || DEFAULT_SEND_FILE_DISPOSITION
end
end
参考: ruby - what is X-Cascade header - Stack Overflow
「修正の対象はRangeヘッダーの方ですね↓」「Rangeヘッダーの値が無効な場合のエラーとして404ではなく416を返す、なるほど」「HTTPの仕様なら直すべきですね👍」
# activestorage/test/controllers/disk_controller_test.rb#35
test "showing blob with invalid range" do
blob = create_blob
get blob.url, headers: { "Range" => "bytes=1000-1000" }
assert_response :range_not_satisfiable
end
参考: 416 Range Not Satisfiable - HTTP | MDN
参考: 404 Not Found - HTTP | MDN
「ところで、HTTP 1.1で追加されたRangeヘッダーは偉大な機能だなと改めて思いますね: このヘッダーのおかげで、ブラウザでダウンロードを中断しても後から再開できるようになった」「回線が遅い時代は特に助かりましたね」「Rangeヘッダーは、ここでやっているようにダウンロードを中断・再開するときに使われます」
「そういえばファイルストリームをNginxにパススルーするときに昔はX-Accel-Redirectヘッダーを使いましたね↓」「そうそう、セキュリティに気を遣うし、Nginx側にファイルを置かないとできなかったけど」「現代はS3のようなオブジェクトストレージサービスが全盛なので、X-Accel-Redirectヘッダーはだんだん使われなくなっているかもしれませんね」
参考: S3のファイルをX-Accel-Redirectで配信する | Money Forward Money Forward Engineers' Blog
「X-Accel-Redirectヘッダーを使うとしたらAWSを使えないプロジェクトとかかな」「そういう場合はしょうがないですね」「そういうプロジェクトで、X-Accel-Redirectヘッダーを付けないとiOSで動画をダウンロードできなかったことがあったんですが、Rangeヘッダーに対応してなかったのが原因でした」
🔗 ActiveRecord::QueryMethods#select
にハッシュを渡せるようになった
#select
メソッドにカラムやエイリアス名をハッシュで渡せる機能を追加した。
Post
.joins(:comments)
.select(
posts: { id: :post_id, title: :post_title },
comments: { id: :comment_id, body: :comment_body}
)
# instead of:
Post
.joins(:comments)
.select(
"posts.id as post_id, posts.title as post_title,
comments.id as comment_id, comments.body as comment_body"
)
概要
#select
引数に渡したhash
引数を文字列に変換して#select_values
がディープに変更されるのを回避する処理を追加。
その他情報
#where
にもとても似たインターフェイスがある。たとえば#joins
と以下のように組み合わせられる。
Post.joins(:comments).where(comments: { author_id: 1 })
ならばこの機能も明確で理解しやすいだろう。
同PRより
つっつきボイス:「お、select
でposts: { id: :post_id, title: :post_title }
のようなハッシュを渡せるようになるのか↓」「これは嬉しい🎉」「この書き方はできてもいいですね」「言われてみれば、where
でできる書き方がselect
でできなかったのが不思議なくらい」
Post
.joins(:comments)
.select(
posts: { id: :post_id, title: :post_title },
comments: { id: :comment_id, body: :comment_body}
)
参考: Rails API select
-- ActiveRecord::QueryMethods
「この書き方はSQLのAS
のシンタックスシュガーっぽいのかな」「でしょうね: 今まではAS
を使おうとすると以下のように生SQLを書くかArelでやるしかなかったけど、上のようにRubyらしく書けるのはいいですね👍」「これはもう使うしかないヤツ」「ちなみにposts: [:id, :title, :created_at]
のように配列を渡せばAS
は使われないのね」
# instead of:
Post
.joins(:comments)
.select(
"posts.id as post_id, posts.title as post_title,
comments.id as comment_id, comments.body as comment_body"
)
「余談ですけど、2番目のコードの生SQLが小文字で書かれていて最初SQLってわからなかった😅」「SQL予約語は、大文字で書く人も小文字で書く人もいますよね」「自分は大文字で書きたいかな」「自分はどっちでも」「SQL関数の視認性が高まるので揃えるなら大文字かな」
🔗 ActiveSupport::Cache::Store#fetch
のブロックでoptions
アクセサを渡せるようになった
これによってキャッシュオプションをオーバーライドできるようになる。
Rails.cache.fetch("3rd-party-token") do |name, options|
token = fetch_token_from_remote
# キャッシュのTTLをトークンのTTLに合わせる
options.expires_in = token.expires_in
token
end
Andrii Gladkyi, Jean Boussier
同Changelogより
これは、寿命の短い認証トークンを扱うAPIで非常によくあるユースケース。
この実装は@argによるものとほとんど同じだが、オプションのハッシュを渡す代わりに、制限されたインターフェイスを持つオブジェクトをyield
する点が異なる。
すべてのオブジェクトが読み込みと書き込みを切り替えられるとは限らないので、ユーザーを驚かせないようインターフェイスを制限している。
共著: @arg
同PRより
つっつきボイス:「fetch
にブロックを渡したときにブロック引数にoptions
を渡せるようになったんですね」「この書き方ができるとprocなどをoptions
に渡してブロック内で使いやすくなる👍」「こういう書き方好き❤️」
Rails.cache.fetch("3rd-party-token") do |name, options|
token = fetch_token_from_remote
# キャッシュのTTLをトークンのTTLに合わせる
options.expires_in = token.expires_in
token
end
「他の言語ではあまり見かけないRubyならではの文化というか書き方ですよね」「コードにもあるように、yield
を使うのがRuby独特↓」「他の言語から来た人はyield
で最初戸惑うかもしれないけど、そこから先はだいたい同じにやれるかなと思います」「JavaだとRunnable
で似たようなことができそうだけど、引数を渡せなかったりいろいろ面倒そう」「わかります」
# activesupport/lib/active_support/cache.rb#L858
def save_block_result_to_cache(name, options)
result = instrument(:generate, name, options) do
- yield(name)
+ yield(name, WriteOptions.new(options))
end
write(name, result, options) unless result.nil? && options[:skip_nil]
result
end
参考: Runnable
(Java Platform SE 8 )
🔗 rails-ujsコードをES2015モジュールに変換
概要
このプルリクは多数の細かいコミットに分けてある(Git履歴を読みやすくするのと、手動変更と自動変更を区別するため)。
- .coffeeファイルを移動およびリネーム(履歴用)
decaffeinate
メソッドで.coffeeを.jsに変換(自動)decaffeinate
のwarningをクリーンアップ(手動)- bladeとsprocketsによるビルドをrollupに置き換え(手動)
その他情報
bladeを完全に削除するには今後もう少し作業が必要(ujsテストで使われているため)だが、このプルリクでは既に十分な規模で削除してある。
関連PR: #34177
修正issue: #45407
つっつきボイス:「CoffeeScriptのコードがとうとう削除されたんですね」「.coffeeファイルが軒並み置き換わってる」
「今思うとCoffeeScriptって何だったんでしょうね」「現代でCoffeeScriptを新たに使うことはないと思いますけど、CoffeeScriptが登場したことがきっかけでAltJSがいろいろ誕生して、そこから現代の書きやすい素のJavaScriptにつながったという側面もあると思いますよ」「それもそうですね」「昔のJavaScriptは人間が書くときにあまりに間違えやすくて、そこをカバーするためにAltJSが生まれましたけど、その中でもCoffeeScriptはRailsに取り入れられたこともあってproductionで実際に使われるようになって、それによってAltJSが認知されて広まったという功績はあったと思います」「たしかに今のJavaScriptは昔とは比べ物にならないぐらい良くなりましたよね」
参考: AltJS -- JavaScript - Wikipedia
🔗 ドキュメント更新2件
- PR: Update guides for update_all by dacook · Pull Request #45853 · rails/rails
- PR: [ci-skip] Add a new Rails guide page for Error Reporter by st0012 · Pull Request #45946 · rails/rails
つっつきボイス:「1つ目の更新される前のupdate_all
のドキュメント、:all
を渡す書き方はたしかに古い↓」「Rails 2あたりの時代かな?」
-User.update(:all, max_login_attempts: 3, must_change_password: true)
+User.update_all max_login_attempts: 3, must_change_password: true
「2つ目はError Reporterのガイドが新たに追加されていました」「どこかで見たような気がしたけど、以前ウォッチでErrorReporter
が取り上げていた(ウォッチ20211129)」「Rails 7のみが対象で、Rails.error
で以下のようにhandle
やrecord
とかが書けるんですね」
Rails.error.record do
do_something!
end
参考: Rails API ActiveSupport::ErrorReporter
🔗Rails
🔗 Rack 3へのアップグレードガイド(Rails公式ニュースより)
つっつきボイス:「先週取り上げたRack 3(ウォッチ20220920)のアップグレードガイドです」「work in progressとあるからまだ作業中ですね」
「変更内容を見てみると、Rack 2と3で結構変わっている部分あるな〜」「レスポンスの配列やheaders
がfrozenにできなくなって、status
が100以下の整数値のみになってる」「特に長く使われているアプリだとRackのメジャーアップデートはそれなりに工数がかかりそう...」
🔗 stimpack: packwerkをさらに使いやすく
We’ve just started trialing packwerk and stimpack for a service in our monolith. Looking forward to seeing how it pans out.
— Richard MacCaw (@rmaccaw) September 13, 2022
つっつきボイス:「先週取り上げたpackwerk(ウォッチ20220920)のツイート↑で言及されていたstimpackを取り上げてみました」「stimpackというとFalloutのこれを思い出す↓」「元々はSF映画とかに登場する回復用携帯注射器を指すんですね」
My newest #3Dprinting project, a #Fallout stimpack pic.twitter.com/GeK15omJCs
— MrGlayden (@glayden_mr) February 27, 2021
参考: Fallout シリーズ - Wikipedia
参考: Urban Dictionary: stimpak
「このstimpackはpackwerkの上に乗っけて使うそうです」「あの後packwerkのドキュメントを見てみたけどなかなかのボリュームなんですよ」「そうそう、今packwerkのドキュメントを翻訳中なんですが結構長いですね」「stimpackを乗せるとpackwerkのコンフィグ周りとかが使いやすくなるならチェックしてみてもいいかも」「packwerkやstimpackは大規模なアプリケーションを想定しているので、評価するにはそういうプロジェクトで実際に使ってみる必要があるでしょうね」
これは僕が求めていたものな気がする。巨大なRailsアプリを段階的に分割できる / rubyatscale/stimpack: stimpack establishes and implements a set of conventions for splitting up large monoliths https://t.co/b4tuEblrjv
— Shun Sugai (@sugaishun) July 12, 2022
🔗 Howitzer: Rubyで書かれた受け入れテストフレームワーク(Ruby Weeklyより)
つっつきボイス:「Howitzerという受け入れテストフレームワークで、Rubyに限らずWebアプリケーション一般で使えるようです」「ドイツ語っぽい名前」「Howitzerは軍事用語だと榴弾砲だそうです」
参考: 榴弾砲 - Wikipedia
「公式サイトを見るのが早いかも↓」
「なるほど、受け入れテストを以下のように書けるんですね: Capybaraだとvisit '/'
みたいに書くところだけど、それをHomePage.open
とかProductPage.open(id: 1)
のように書ける↓」「というと?」「visit '/'
はテストをベタ書きする点はいいんだけど、それだけだと意味がわかりにくい: HowitzerはSearchPage.open
のように1段階抽象化した形で意味がわかるように書ける」「なるほど」「自分はCapybaraでベタに書く方が好きですけどね」
# 同ガイドより
HomePage.open #=> visits /
ProductPage.open(id: 1) #=> visits /products/1
SearchPage.open #=> visits /search
SearchPage.open(query: {text: :foo}) #=> visits /search?text=foo
「バリデーションのような細かい部分はクラス定義で事前に隠蔽しておくという発想のようですね↓」
# 同ガイドより
# Example 1:
class HomePage < Howitzer::Web::Page
path '/'
validate :url, /\A(?:.*?:\/\/)?[^\/]*\/?\z/
end
# Example 2:
class LoginPage < Howitzer::Web::Page
path '/users/sign_in'
validate :title, /Sign In\z/
end
「最終的にこう書けるようにする、なるほど↓: CucomberやTurnipのように自然言語で受け入れテストを記述するのがつらい人にとっては、こういうふうにRubyのメソッド的に受け入れテストを書きたいかもしれませんね」「その気持すっごくわかります」「Rubyっぽく書けるなら日本語の問題を心配をせずに済みますね」
# 同ガイドより
When /^I navigate to article on article list page$/ do
ArticleListPage.open
ArticleListPage.on { open_article(out(:@article).title) }
end
「個人的には、テストコードがこれ以上複雑になると"テストコードのテスト"が必要になりそうなのが気になるかな」「そうなったら本末転倒ですよね」「こんなふうにテストコードにメソッドを定義するのは避けたい↓」
# 同ガイドより
class HomePage < Howitzer::Web::Page
path '/'
validate :url, /\A(?:.*?:\/\/)?[^\/]*\/?\z/
def method1
#some logic here
self
end
def method2
#some logic here
self
end
end
「"これはクラスの形をしているけどコンフィグなんだ"と思って、メソッド定義やモジュール追加は禁止して使うならいいかも」「でもプログラマーはclass
が使えるならついdef
でメソッド定義したくなるんですよ😆」
「自分はたぶん使わないと思いますが、受け入れテストのために自然言語を大量に書きたくない場合はHowitzerが向いていることもありそうですね」「たしかに」
🔗 その他Rails
つっつきボイス:「N予備校頑張ってますね」「TechRachoの記事が引用されていたので気づきました」
「"let
ではなくlet!
を使う"ルールがありますね」「let!
は上から順に実行されるけど、let
は遅延評価なのでデバッグがつらくなりがちなヤツ」「そうなんですよ」「let
とlet!
はまったく違いますよね」
「ただ、自分ならlet!
を使うよりもbefore
ブロックでインスタンス変数とかに入れるでしょうね」「たとえば初心者向けの学習用ルールだとlet
はデバッグ大変すぎるのでlet!
で書くことにする、ということなら理解できますけど、業務でテスト用のデータを初期化するならlet!
をたくさん書くよりもbefore
ブロックでまとめて初期化する方が一般的かなと自分も思います」
前編は以上です。
バックナンバー(2022年度第3四半期)
週刊Railsウォッチ: Ruby 3.2.0 Preview 2とRack 3.0リリース、packwerkでアプリコードの境界を強制ほか(20220920)
- 20220906後編 syntax_suggestがRuby標準ライブラリに追加、RubyのVisitorパターンほか
- 20220905前編 Herokuが無料プラン廃止を発表、Hotwire日本語コミュニティほか
- 20220830後編 RubyKaigi 2022タイムテーブル公開、viewport-extraほか
- 20220829前編 MinitestとRSpecの比較、商用版NGINXの重要機能がオープンソース化ほか
- 20220823後編 byebugからruby/debugへの移行ガイド、YJIT解説記事ほ
- 20220822前編 ビューテンプレートに渡せるローカル変数をマジックコメントでチェック可能にほか
- 20220802後編 RubyのGVLトレーサーgvl-tracing、casting gemでオブジェクトに振る舞いを追加ほか
- 20220801前編 “リーダブルテストコードについて考えよう”スライド公開、Evil Martiansが日本上陸ほか
- 20220726後編 中高生国際Rubyプログラミングコンテスト2022、W3Cの分散型識別子仕様が勧告にほか
- 20220725前編 RailsConf 2022の動画が公開、マイクロサービスのテスト戦略ほか(
- 20220719 RubyのGCが高速化、RuboCopのストレスを減らす4つの方法、Defensive CSSほか
- 20220711前編 AR::RelationにCTEを利用できるwithメソッドが追加、Propshaftアップグレードガイドほか
- 20220705後編 6月のRubyコア動向、Stack Overflowアンケート結果ほか
- 20220704前編 マイグレーションをStrategyパターンで拡張可能にほか
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。
週刊Railsウォッチについて
TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)