From 38f5e74122f8a863b8c240fc69d38dd7c852fb17 Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 25 Mar 2025 13:30:10 +0100 Subject: [PATCH] Add `Deprecation` headers on deprecated endpoints (#34262) Co-authored-by: Damien Mathieu <42@dmathieu.com> --- .../v1/accounts/identity_proofs_controller.rb | 4 ++ app/controllers/api/v1/filters_controller.rb | 4 ++ .../api/v1/instances_controller.rb | 12 ++--- .../api/v1/suggestions_controller.rb | 3 ++ .../api/v1/trends/tags_controller.rb | 4 ++ .../api/v2/instances_controller.rb | 12 ++++- .../concerns/deprecation_concern.rb | 17 +++++++ app/javascript/mastodon/api.ts | 25 +++++++++- spec/requests/api/v1/trends/tags_spec.rb | 4 ++ spec/requests/api/v1/trends_spec.rb | 48 +++++++++++++++++++ 10 files changed, 121 insertions(+), 12 deletions(-) create mode 100644 app/controllers/concerns/deprecation_concern.rb create mode 100644 spec/requests/api/v1/trends_spec.rb diff --git a/app/controllers/api/v1/accounts/identity_proofs_controller.rb b/app/controllers/api/v1/accounts/identity_proofs_controller.rb index 48f293f47a..02a45e8758 100644 --- a/app/controllers/api/v1/accounts/identity_proofs_controller.rb +++ b/app/controllers/api/v1/accounts/identity_proofs_controller.rb @@ -1,6 +1,10 @@ # frozen_string_literal: true class Api::V1::Accounts::IdentityProofsController < Api::BaseController + include DeprecationConcern + + deprecate_api '2022-03-30' + before_action :require_user! before_action :set_account diff --git a/app/controllers/api/v1/filters_controller.rb b/app/controllers/api/v1/filters_controller.rb index ed98acce30..5e9ba5153c 100644 --- a/app/controllers/api/v1/filters_controller.rb +++ b/app/controllers/api/v1/filters_controller.rb @@ -1,6 +1,10 @@ # frozen_string_literal: true class Api::V1::FiltersController < Api::BaseController + include DeprecationConcern + + deprecate_api '2022-11-14' + before_action -> { doorkeeper_authorize! :read, :'read:filters' }, only: [:index, :show] before_action -> { doorkeeper_authorize! :write, :'write:filters' }, except: [:index, :show] before_action :require_user! diff --git a/app/controllers/api/v1/instances_controller.rb b/app/controllers/api/v1/instances_controller.rb index 49da75ed28..e01267c000 100644 --- a/app/controllers/api/v1/instances_controller.rb +++ b/app/controllers/api/v1/instances_controller.rb @@ -1,15 +1,9 @@ # frozen_string_literal: true -class Api::V1::InstancesController < Api::BaseController - skip_before_action :require_authenticated_user!, unless: :limited_federation_mode? - skip_around_action :set_locale +class Api::V1::InstancesController < Api::V2::InstancesController + include DeprecationConcern - vary_by '' - - # Override `current_user` to avoid reading session cookies unless in limited federation mode - def current_user - super if limited_federation_mode? - end + deprecate_api '2022-11-14' def show cache_even_if_authenticated! diff --git a/app/controllers/api/v1/suggestions_controller.rb b/app/controllers/api/v1/suggestions_controller.rb index 9ba1cef63c..918ec45beb 100644 --- a/app/controllers/api/v1/suggestions_controller.rb +++ b/app/controllers/api/v1/suggestions_controller.rb @@ -2,6 +2,9 @@ class Api::V1::SuggestionsController < Api::BaseController include Authorization + include DeprecationConcern + + deprecate_api '2021-05-16' before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, only: :index before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :index diff --git a/app/controllers/api/v1/trends/tags_controller.rb b/app/controllers/api/v1/trends/tags_controller.rb index 10a3442344..f84f1c0252 100644 --- a/app/controllers/api/v1/trends/tags_controller.rb +++ b/app/controllers/api/v1/trends/tags_controller.rb @@ -1,12 +1,16 @@ # frozen_string_literal: true class Api::V1::Trends::TagsController < Api::BaseController + include DeprecationConcern + before_action :set_tags after_action :insert_pagination_headers DEFAULT_TAGS_LIMIT = 10 + deprecate_api '2022-03-30', only: :index, if: -> { request.path == '/api/v1/trends' } + def index cache_if_unauthenticated! render json: @tags, each_serializer: REST::TagSerializer, relationships: TagRelationshipsPresenter.new(@tags, current_user&.account_id) diff --git a/app/controllers/api/v2/instances_controller.rb b/app/controllers/api/v2/instances_controller.rb index 8346e28830..62adf95260 100644 --- a/app/controllers/api/v2/instances_controller.rb +++ b/app/controllers/api/v2/instances_controller.rb @@ -1,6 +1,16 @@ # frozen_string_literal: true -class Api::V2::InstancesController < Api::V1::InstancesController +class Api::V2::InstancesController < Api::BaseController + skip_before_action :require_authenticated_user!, unless: :limited_federation_mode? + skip_around_action :set_locale + + vary_by '' + + # Override `current_user` to avoid reading session cookies unless in limited federation mode + def current_user + super if limited_federation_mode? + end + def show cache_even_if_authenticated! render_with_cache json: InstancePresenter.new, serializer: REST::InstanceSerializer, root: 'instance' diff --git a/app/controllers/concerns/deprecation_concern.rb b/app/controllers/concerns/deprecation_concern.rb new file mode 100644 index 0000000000..ad8de724a1 --- /dev/null +++ b/app/controllers/concerns/deprecation_concern.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module DeprecationConcern + extend ActiveSupport::Concern + + class_methods do + def deprecate_api(date, sunset: nil, **kwargs) + deprecation_timestamp = "@#{date.to_datetime.to_i}" + sunset = sunset&.to_date&.httpdate + + before_action(**kwargs) do + response.headers['Deprecation'] = deprecation_timestamp + response.headers['Sunset'] = sunset if sunset + end + end + end +end diff --git a/app/javascript/mastodon/api.ts b/app/javascript/mastodon/api.ts index f0663ded40..a41b058d2c 100644 --- a/app/javascript/mastodon/api.ts +++ b/app/javascript/mastodon/api.ts @@ -1,4 +1,9 @@ -import type { AxiosResponse, Method, RawAxiosRequestHeaders } from 'axios'; +import type { + AxiosError, + AxiosResponse, + Method, + RawAxiosRequestHeaders, +} from 'axios'; import axios from 'axios'; import LinkHeader from 'http-link-header'; @@ -41,7 +46,7 @@ const authorizationTokenFromInitialState = (): RawAxiosRequestHeaders => { // eslint-disable-next-line import/no-default-export export default function api(withAuthorization = true) { - return axios.create({ + const instance = axios.create({ transitional: { clarifyTimeoutError: true, }, @@ -60,6 +65,22 @@ export default function api(withAuthorization = true) { }, ], }); + + instance.interceptors.response.use( + (response: AxiosResponse) => { + if (response.headers.deprecation) { + console.warn( + `Deprecated request: ${response.config.method} ${response.config.url}`, + ); + } + return response; + }, + (error: AxiosError) => { + return Promise.reject(error); + }, + ); + + return instance; } type RequestParamsOrData = Record; diff --git a/spec/requests/api/v1/trends/tags_spec.rb b/spec/requests/api/v1/trends/tags_spec.rb index 14ab73fc96..097393e58d 100644 --- a/spec/requests/api/v1/trends/tags_spec.rb +++ b/spec/requests/api/v1/trends/tags_spec.rb @@ -15,6 +15,8 @@ RSpec.describe 'API V1 Trends Tags' do .and not_have_http_link_header expect(response.content_type) .to start_with('application/json') + expect(response.headers['Deprecation']) + .to be_nil end end @@ -31,6 +33,8 @@ RSpec.describe 'API V1 Trends Tags' do .and have_http_link_header(api_v1_trends_tags_url(offset: 2)).for(rel: 'next') expect(response.content_type) .to start_with('application/json') + expect(response.headers['Deprecation']) + .to be_nil end def prepare_trends diff --git a/spec/requests/api/v1/trends_spec.rb b/spec/requests/api/v1/trends_spec.rb new file mode 100644 index 0000000000..5bfabdca1c --- /dev/null +++ b/spec/requests/api/v1/trends_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'deprecated API V1 Trends Tags' do + describe 'GET /api/v1/trends' do + context 'when trends are disabled' do + before { Setting.trends = false } + + it 'returns http success' do + get '/api/v1/trends' + + expect(response) + .to have_http_status(200) + .and not_have_http_link_header + expect(response.content_type) + .to start_with('application/json') + expect(response.headers['Deprecation']) + .to start_with '@' + end + end + + context 'when trends are enabled' do + before { Setting.trends = true } + + it 'returns http success' do + prepare_trends + stub_const('Api::V1::Trends::TagsController::DEFAULT_TAGS_LIMIT', 2) + get '/api/v1/trends' + + expect(response) + .to have_http_status(200) + .and have_http_link_header(api_v1_trends_tags_url(offset: 2)).for(rel: 'next') + expect(response.content_type) + .to start_with('application/json') + expect(response.headers['Deprecation']) + .to start_with '@' + end + + def prepare_trends + Fabricate.times(3, :tag, trendable: true).each do |tag| + 2.times { |i| Trends.tags.add(tag, i) } + end + Trends::Tags.new(threshold: 1).refresh + end + end + end +end