diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex
index c85a8b09f..41587c116 100644
--- a/lib/pleroma/object/fetcher.ex
+++ b/lib/pleroma/object/fetcher.ex
@@ -1,219 +1,257 @@
 # Pleroma: A lightweight social networking server
 # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
 # SPDX-License-Identifier: AGPL-3.0-only
 
 defmodule Pleroma.Object.Fetcher do
   alias Pleroma.HTTP
   alias Pleroma.Instances
   alias Pleroma.Maps
   alias Pleroma.Object
   alias Pleroma.Object.Containment
   alias Pleroma.Signature
   alias Pleroma.Web.ActivityPub.InternalFetchActor
   alias Pleroma.Web.ActivityPub.MRF
   alias Pleroma.Web.ActivityPub.ObjectValidator
   alias Pleroma.Web.ActivityPub.Pipeline
   alias Pleroma.Web.ActivityPub.Transmogrifier
   alias Pleroma.Web.Federator
 
   require Logger
   require Pleroma.Constants
 
+  @mix_env Mix.env()
+
   @spec reinject_object(struct(), map()) :: {:ok, Object.t()} | {:error, any()}
   defp reinject_object(%Object{data: %{}} = object, new_data) do
     Logger.debug("Reinjecting object #{new_data["id"]}")
 
     with {:ok, new_data, _} <- ObjectValidator.validate(new_data, %{}),
          {:ok, new_data} <- MRF.filter(new_data),
          {:ok, new_object, _} <-
            Object.Updater.do_update_and_invalidate_cache(
              object,
              new_data,
              _touch_changeset? = true
            ) do
       {:ok, new_object}
     else
       e ->
         Logger.error("Error while processing object: #{inspect(e)}")
         {:error, e}
     end
   end
 
   defp reinject_object(_, new_data) do
     with {:ok, object, _} <- Pipeline.common_pipeline(new_data, local: false) do
       {:ok, object}
     else
       e -> e
     end
   end
 
   def refetch_object(%Object{data: %{"id" => id}} = object) do
     with {:local, false} <- {:local, Object.local?(object)},
          {:ok, new_data} <- fetch_and_contain_remote_object_from_id(id),
          {:ok, object} <- reinject_object(object, new_data) do
       {:ok, object}
     else
       {:local, true} -> {:ok, object}
       e -> {:error, e}
     end
   end
 
   @typep fetcher_errors ::
            :error | :reject | :allowed_depth | :fetch | :containment | :transmogrifier
 
   # Note: will create a Create activity, which we need internally at the moment.
   @spec fetch_object_from_id(String.t(), list()) ::
           {:ok, Object.t()} | {fetcher_errors(), any()} | Pipeline.errors()
   def fetch_object_from_id(id, options \\ []) do
     with {_, nil} <- {:fetch_object, Object.get_cached_by_ap_id(id)},
          {_, true} <- {:allowed_depth, Federator.allowed_thread_distance?(options[:depth])},
          {_, {:ok, data}} <- {:fetch, fetch_and_contain_remote_object_from_id(id)},
          {_, nil} <- {:normalize, Object.normalize(data, fetch: false)},
          params <- prepare_activity_params(data),
          {_, :ok} <- {:containment, Containment.contain_origin(id, params)},
          {_, {:ok, activity}} <-
            {:transmogrifier, Transmogrifier.handle_incoming(params, options)},
          {_, _data, %Object{} = object} <-
            {:object, data, Object.normalize(activity, fetch: false)} do
       {:ok, object}
     else
       {:normalize, object = %Object{}} ->
         {:ok, object}
 
       {:fetch_object, %Object{} = object} ->
         {:ok, object}
 
       {:object, data, nil} ->
         reinject_object(%Object{}, data)
 
       e ->
         Logger.metadata(object: id)
         Logger.error("Object rejected while fetching #{id} #{inspect(e)}")
         e
     end
   end
 
   defp prepare_activity_params(data) do
     %{
       "type" => "Create",
       # Should we seriously keep this attributedTo thing?
       "actor" => data["actor"] || data["attributedTo"],
       "object" => data
     }
     |> Maps.put_if_present("to", data["to"])
     |> Maps.put_if_present("cc", data["cc"])
     |> Maps.put_if_present("bto", data["bto"])
     |> Maps.put_if_present("bcc", data["bcc"])
   end
 
   defp make_signature(id, date) do
     uri = URI.parse(id)
 
     signature =
       InternalFetchActor.get_actor()
       |> Signature.sign(%{
         "(request-target)": "get #{uri.path}",
         host: uri.host,
         date: date
       })
 
     {"signature", signature}
   end
 
   defp sign_fetch(headers, id, date) do
     if Pleroma.Config.get([:activitypub, :sign_object_fetches]) do
       [make_signature(id, date) | headers]
     else
       headers
     end
   end
 
   defp maybe_date_fetch(headers, date) do
     if Pleroma.Config.get([:activitypub, :sign_object_fetches]) do
       [{"date", date} | headers]
     else
       headers
     end
   end
 
   def fetch_and_contain_remote_object_from_id(id)
 
   def fetch_and_contain_remote_object_from_id(%{"id" => id}),
     do: fetch_and_contain_remote_object_from_id(id)
 
   def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do
     Logger.debug("Fetching object #{id} via AP")
 
     with {:scheme, true} <- {:scheme, String.starts_with?(id, "http")},
          {_, true} <- {:mrf, MRF.id_filter(id)},
          {:ok, body} <- get_object(id),
          {:ok, data} <- safe_json_decode(body),
          :ok <- Containment.contain_origin_from_id(id, data) do
       if not Instances.reachable?(id) do
         Instances.set_reachable(id)
       end
 
       {:ok, data}
     else
       {:scheme, _} ->
         {:error, "Unsupported URI scheme"}
 
       {:error, e} ->
         {:error, e}
 
       {:mrf, false} ->
         {:error, {:reject, "Filtered by id"}}
 
       e ->
         {:error, e}
     end
   end
 
   def fetch_and_contain_remote_object_from_id(_id),
     do: {:error, "id must be a string"}
 
+  defp check_crossdomain_redirect(final_host, original_url)
+
+  # Handle the common case in tests where responses don't include URLs
+  if @mix_env == :test do
+    defp check_crossdomain_redirect(nil, _) do
+      {:cross_domain_redirect, false}
+    end
+  end
+
+  defp check_crossdomain_redirect(final_host, original_url) do
+    {:cross_domain_redirect, final_host != URI.parse(original_url).host}
+  end
+
   defp get_object(id) do
     date = Pleroma.Signature.signed_date()
 
     headers =
       [{"accept", "application/activity+json"}]
       |> maybe_date_fetch(date)
       |> sign_fetch(id, date)
 
     case HTTP.get(id, headers) do
+      {:ok, %{body: body, status: code, headers: headers, url: final_url}}
+      when code in 200..299 ->
+        remote_host = if final_url, do: URI.parse(final_url).host, else: nil
+
+        with {:cross_domain_redirect, false} <- check_crossdomain_redirect(remote_host, id),
+             {_, content_type} <- List.keyfind(headers, "content-type", 0),
+             {:ok, _media_type} <- verify_content_type(content_type) do
+          {:ok, body}
+        else
+          {:cross_domain_redirect, true} ->
+            {:error, {:cross_domain_redirect, true}}
+
+          error ->
+            error
+        end
+
+      # Handle the case where URL is not in the response (older HTTP library versions)
       {:ok, %{body: body, status: code, headers: headers}} when code in 200..299 ->
         case List.keyfind(headers, "content-type", 0) do
           {_, content_type} ->
-            case Plug.Conn.Utils.media_type(content_type) do
-              {:ok, "application", "activity+json", _} ->
-                {:ok, body}
-
-              {:ok, "application", "ld+json",
-               %{"profile" => "https://www.w3.org/ns/activitystreams"}} ->
-                {:ok, body}
-
-              _ ->
-                {:error, {:content_type, content_type}}
+            case verify_content_type(content_type) do
+              {:ok, _} -> {:ok, body}
+              error -> error
             end
 
           _ ->
             {:error, {:content_type, nil}}
         end
 
       {:ok, %{status: code}} when code in [401, 403] ->
         {:error, :forbidden}
 
       {:ok, %{status: code}} when code in [404, 410] ->
         {:error, :not_found}
 
       {:error, e} ->
         {:error, e}
 
       e ->
         {:error, e}
     end
   end
 
   defp safe_json_decode(nil), do: {:ok, nil}
   defp safe_json_decode(json), do: Jason.decode(json)
+
+  defp verify_content_type(content_type) do
+    case Plug.Conn.Utils.media_type(content_type) do
+      {:ok, "application", "activity+json", _} ->
+        {:ok, :activity_json}
+
+      {:ok, "application", "ld+json", %{"profile" => "https://www.w3.org/ns/activitystreams"}} ->
+        {:ok, :ld_json}
+
+      _ ->
+        {:error, {:content_type, content_type}}
+    end
+  end
 end
diff --git a/test/pleroma/object/fetcher_test.exs b/test/pleroma/object/fetcher_test.exs
index 215fca570..4dabc283a 100644
--- a/test/pleroma/object/fetcher_test.exs
+++ b/test/pleroma/object/fetcher_test.exs
@@ -1,616 +1,720 @@
 # Pleroma: A lightweight social networking server
 # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
 # SPDX-License-Identifier: AGPL-3.0-only
 
 defmodule Pleroma.Object.FetcherTest do
   use Pleroma.DataCase
 
   alias Pleroma.Activity
   alias Pleroma.Instances
   alias Pleroma.Object
   alias Pleroma.Object.Fetcher
   alias Pleroma.Web.ActivityPub.ObjectValidator
 
   require Pleroma.Constants
 
   import Mock
   import Pleroma.Factory
   import Tesla.Mock
 
   setup do
     mock(fn
       %{method: :get, url: "https://mastodon.example.org/users/userisgone"} ->
         %Tesla.Env{status: 410}
 
       %{method: :get, url: "https://mastodon.example.org/users/userisgone404"} ->
         %Tesla.Env{status: 404}
 
       %{
         method: :get,
         url:
           "https://patch.cx/media/03ca3c8b4ac3ddd08bf0f84be7885f2f88de0f709112131a22d83650819e36c2.json"
       } ->
         %Tesla.Env{
           status: 200,
           headers: [{"content-type", "application/json"}],
           body: File.read!("test/fixtures/spoofed-object.json")
         }
 
       env ->
         apply(HttpRequestMock, :request, [env])
     end)
 
     :ok
   end
 
   describe "error cases" do
     setup do
       mock(fn
         %{method: :get, url: "https://social.sakamoto.gq/notice/9wTkLEnuq47B25EehM"} ->
           %Tesla.Env{
             status: 200,
             body: File.read!("test/fixtures/fetch_mocks/9wTkLEnuq47B25EehM.json"),
             headers: HttpRequestMock.activitypub_object_headers()
           }
 
         %{method: :get, url: "https://social.sakamoto.gq/users/eal"} ->
           %Tesla.Env{
             status: 200,
             body: File.read!("test/fixtures/fetch_mocks/eal.json"),
             headers: HttpRequestMock.activitypub_object_headers()
           }
 
         %{method: :get, url: "https://busshi.moe/users/tuxcrafting/statuses/104410921027210069"} ->
           %Tesla.Env{
             status: 200,
             body: File.read!("test/fixtures/fetch_mocks/104410921027210069.json"),
             headers: HttpRequestMock.activitypub_object_headers()
           }
 
         %{method: :get, url: "https://busshi.moe/users/tuxcrafting"} ->
           %Tesla.Env{
             status: 500
           }
 
         %{
           method: :get,
           url: "https://stereophonic.space/objects/02997b83-3ea7-4b63-94af-ef3aa2d4ed17"
         } ->
           %Tesla.Env{
             status: 500
           }
       end)
 
       :ok
     end
 
     test "it works when fetching the OP actor errors out" do
       # Here we simulate a case where the author of the OP can't be read
       assert {:ok, _} =
                Fetcher.fetch_object_from_id(
                  "https://social.sakamoto.gq/notice/9wTkLEnuq47B25EehM"
                )
     end
   end
 
   describe "max thread distance restriction" do
     @ap_id "http://mastodon.example.org/@admin/99541947525187367"
     setup do: clear_config([:instance, :federation_incoming_replies_max_depth])
 
     test "it returns thread depth exceeded error if thread depth is exceeded" do
       clear_config([:instance, :federation_incoming_replies_max_depth], 0)
 
       assert {:allowed_depth, false} = Fetcher.fetch_object_from_id(@ap_id, depth: 1)
     end
 
     test "it fetches object if max thread depth is restricted to 0 and depth is not specified" do
       clear_config([:instance, :federation_incoming_replies_max_depth], 0)
 
       assert {:ok, _} = Fetcher.fetch_object_from_id(@ap_id)
     end
 
     test "it fetches object if requested depth does not exceed max thread depth" do
       clear_config([:instance, :federation_incoming_replies_max_depth], 10)
 
       assert {:ok, _} = Fetcher.fetch_object_from_id(@ap_id, depth: 10)
     end
   end
 
   describe "actor origin containment" do
     test "it rejects objects with a bogus origin" do
       {:containment, :error} =
         Fetcher.fetch_object_from_id("https://info.pleroma.site/activity.json")
     end
 
     test "it rejects objects when attributedTo is wrong (variant 1)" do
       {:containment, :error} =
         Fetcher.fetch_object_from_id("https://info.pleroma.site/activity2.json")
     end
 
     test "it rejects objects when attributedTo is wrong (variant 2)" do
       {:containment, :error} =
         Fetcher.fetch_object_from_id("https://info.pleroma.site/activity3.json")
     end
   end
 
   describe "fetching an object" do
     test "it fetches an object" do
       {:ok, object} =
         Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
 
       assert _activity = Activity.get_create_by_object_ap_id(object.data["id"])
 
       {:ok, object_again} =
         Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
 
       assert [attachment] = object.data["attachment"]
       assert is_list(attachment["url"])
 
       assert object == object_again
     end
 
     test "Return MRF reason when fetched status is rejected by one" do
       clear_config([:mrf_keyword, :reject], ["yeah"])
       clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy])
 
       assert {:transmogrifier, {:reject, "[KeywordPolicy] Matches with rejected keyword"}} ==
                Fetcher.fetch_object_from_id(
                  "http://mastodon.example.org/@admin/99541947525187367"
                )
     end
 
     test "it does not fetch a spoofed object uploaded on an instance as an attachment" do
       assert {:fetch, {:error, {:content_type, "application/json"}}} =
                Fetcher.fetch_object_from_id(
                  "https://patch.cx/media/03ca3c8b4ac3ddd08bf0f84be7885f2f88de0f709112131a22d83650819e36c2.json"
                )
     end
 
     test "it resets instance reachability on successful fetch" do
       id = "http://mastodon.example.org/@admin/99541947525187367"
       Instances.set_consistently_unreachable(id)
       refute Instances.reachable?(id)
 
       {:ok, _object} =
         Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
 
       assert Instances.reachable?(id)
     end
   end
 
   describe "implementation quirks" do
     test "it can fetch plume articles" do
       {:ok, object} =
         Fetcher.fetch_object_from_id(
           "https://baptiste.gelez.xyz/~/PlumeDevelopment/this-month-in-plume-june-2018/"
         )
 
       assert object
     end
 
     test "it can fetch peertube videos" do
       {:ok, object} =
         Fetcher.fetch_object_from_id(
           "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3"
         )
 
       assert object
     end
 
     test "it can fetch Mobilizon events" do
       {:ok, object} =
         Fetcher.fetch_object_from_id(
           "https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39"
         )
 
       assert object
     end
 
     test "it can fetch wedistribute articles" do
       {:ok, object} =
         Fetcher.fetch_object_from_id("https://wedistribute.org/wp-json/pterotype/v1/object/85810")
 
       assert object
     end
 
     test "all objects with fake directions are rejected by the object fetcher" do
       assert {:error, _} =
                Fetcher.fetch_and_contain_remote_object_from_id(
                  "https://info.pleroma.site/activity4.json"
                )
     end
 
     test "handle HTTP 410 Gone response" do
       assert {:error, :not_found} ==
                Fetcher.fetch_and_contain_remote_object_from_id(
                  "https://mastodon.example.org/users/userisgone"
                )
     end
 
     test "handle HTTP 404 response" do
       assert {:error, :not_found} ==
                Fetcher.fetch_and_contain_remote_object_from_id(
                  "https://mastodon.example.org/users/userisgone404"
                )
     end
 
     test "it can fetch pleroma polls with attachments" do
       {:ok, object} =
         Fetcher.fetch_object_from_id("https://patch.cx/objects/tesla_mock/poll_attachment")
 
       assert object
     end
   end
 
   describe "pruning" do
     test "it can refetch pruned objects" do
       object_id = "http://mastodon.example.org/@admin/99541947525187367"
 
       {:ok, object} = Fetcher.fetch_object_from_id(object_id)
 
       assert object
 
       {:ok, _object} = Object.prune(object)
 
       refute Object.get_by_ap_id(object_id)
 
       {:ok, %Object{} = object_two} = Fetcher.fetch_object_from_id(object_id)
 
       assert object.data["id"] == object_two.data["id"]
       assert object.id != object_two.id
     end
   end
 
   describe "signed fetches" do
     setup do: clear_config([:activitypub, :sign_object_fetches])
 
     test_with_mock "it signs fetches when configured to do so",
                    Pleroma.Signature,
                    [:passthrough],
                    [] do
       clear_config([:activitypub, :sign_object_fetches], true)
 
       Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
 
       assert called(Pleroma.Signature.sign(:_, :_))
     end
 
     test_with_mock "it doesn't sign fetches when not configured to do so",
                    Pleroma.Signature,
                    [:passthrough],
                    [] do
       clear_config([:activitypub, :sign_object_fetches], false)
 
       Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
 
       refute called(Pleroma.Signature.sign(:_, :_))
     end
   end
 
   describe "refetching" do
     setup do
       insert(:user, ap_id: "https://mastodon.social/users/emelie")
 
       object1 = %{
         "id" => "https://mastodon.social/1",
         "actor" => "https://mastodon.social/users/emelie",
         "attributedTo" => "https://mastodon.social/users/emelie",
         "type" => "Note",
         "content" => "test 1",
         "bcc" => [],
         "bto" => [],
         "cc" => [],
         "to" => [Pleroma.Constants.as_public()],
         "summary" => "",
         "published" => "2023-05-08 23:43:20Z",
         "updated" => "2023-05-09 23:43:20Z"
       }
 
       {:ok, local_object1, _} = ObjectValidator.validate(object1, [])
 
       object2 = %{
         "id" => "https://mastodon.social/2",
         "actor" => "https://mastodon.social/users/emelie",
         "attributedTo" => "https://mastodon.social/users/emelie",
         "type" => "Note",
         "content" => "test 2",
         "bcc" => [],
         "bto" => [],
         "cc" => [],
         "to" => [Pleroma.Constants.as_public()],
         "summary" => "",
         "published" => "2023-05-08 23:43:20Z",
         "updated" => "2023-05-09 23:43:25Z",
         "formerRepresentations" => %{
           "type" => "OrderedCollection",
           "orderedItems" => [
             %{
               "type" => "Note",
               "content" => "orig 2",
               "actor" => "https://mastodon.social/users/emelie",
               "attributedTo" => "https://mastodon.social/users/emelie",
               "bcc" => [],
               "bto" => [],
               "cc" => [],
               "to" => [Pleroma.Constants.as_public()],
               "summary" => "",
               "published" => "2023-05-08 23:43:20Z",
               "updated" => "2023-05-09 23:43:21Z"
             }
           ],
           "totalItems" => 1
         }
       }
 
       {:ok, local_object2, _} = ObjectValidator.validate(object2, [])
 
       mock(fn
         %{
           method: :get,
           url: "https://mastodon.social/1"
         } ->
           %Tesla.Env{
             status: 200,
             headers: [{"content-type", "application/activity+json"}],
             body: Jason.encode!(object1 |> Map.put("updated", "2023-05-09 23:44:20Z"))
           }
 
         %{
           method: :get,
           url: "https://mastodon.social/2"
         } ->
           %Tesla.Env{
             status: 200,
             headers: [{"content-type", "application/activity+json"}],
             body: Jason.encode!(object2 |> Map.put("updated", "2023-05-09 23:44:20Z"))
           }
 
         %{
           method: :get,
           url: "https://mastodon.social/users/emelie/collections/featured"
         } ->
           %Tesla.Env{
             status: 200,
             headers: [{"content-type", "application/activity+json"}],
             body:
               Jason.encode!(%{
                 "id" => "https://mastodon.social/users/emelie/collections/featured",
                 "type" => "OrderedCollection",
                 "actor" => "https://mastodon.social/users/emelie",
                 "attributedTo" => "https://mastodon.social/users/emelie",
                 "orderedItems" => [],
                 "totalItems" => 0
               })
           }
 
         env ->
           apply(HttpRequestMock, :request, [env])
       end)
 
       %{object1: local_object1, object2: local_object2}
     end
 
     test "it keeps formerRepresentations if remote does not have this attr", %{object1: object1} do
       full_object1 =
         object1
         |> Map.merge(%{
           "formerRepresentations" => %{
             "type" => "OrderedCollection",
             "orderedItems" => [
               %{
                 "type" => "Note",
                 "content" => "orig 2",
                 "actor" => "https://mastodon.social/users/emelie",
                 "attributedTo" => "https://mastodon.social/users/emelie",
                 "bcc" => [],
                 "bto" => [],
                 "cc" => [],
                 "to" => [Pleroma.Constants.as_public()],
                 "summary" => "",
                 "published" => "2023-05-08 23:43:20Z"
               }
             ],
             "totalItems" => 1
           }
         })
 
       {:ok, o} = Object.create(full_object1)
 
       assert {:ok, refetched} = Fetcher.refetch_object(o)
 
       assert %{"formerRepresentations" => %{"orderedItems" => [%{"content" => "orig 2"}]}} =
                refetched.data
     end
 
     test "it uses formerRepresentations from remote if possible", %{object2: object2} do
       {:ok, o} = Object.create(object2)
 
       assert {:ok, refetched} = Fetcher.refetch_object(o)
 
       assert %{"formerRepresentations" => %{"orderedItems" => [%{"content" => "orig 2"}]}} =
                refetched.data
     end
 
     test "it replaces formerRepresentations with the one from remote", %{object2: object2} do
       full_object2 =
         object2
         |> Map.merge(%{
           "content" => "mew mew #def",
           "formerRepresentations" => %{
             "type" => "OrderedCollection",
             "orderedItems" => [
               %{"type" => "Note", "content" => "mew mew 2"}
             ],
             "totalItems" => 1
           }
         })
 
       {:ok, o} = Object.create(full_object2)
 
       assert {:ok, refetched} = Fetcher.refetch_object(o)
 
       assert %{
                "content" => "test 2",
                "formerRepresentations" => %{"orderedItems" => [%{"content" => "orig 2"}]}
              } = refetched.data
     end
 
     test "it adds to formerRepresentations if the remote does not have one and the object has changed",
          %{object1: object1} do
       full_object1 =
         object1
         |> Map.merge(%{
           "content" => "mew mew #def",
           "formerRepresentations" => %{
             "type" => "OrderedCollection",
             "orderedItems" => [
               %{"type" => "Note", "content" => "mew mew 1"}
             ],
             "totalItems" => 1
           }
         })
 
       {:ok, o} = Object.create(full_object1)
 
       assert {:ok, refetched} = Fetcher.refetch_object(o)
 
       assert %{
                "content" => "test 1",
                "formerRepresentations" => %{
                  "orderedItems" => [
                    %{"content" => "mew mew #def"},
                    %{"content" => "mew mew 1"}
                  ],
                  "totalItems" => 2
                }
              } = refetched.data
     end
 
     test "it keeps the history intact if only updated time has changed",
          %{object1: object1} do
       full_object1 =
         object1
         |> Map.merge(%{
           "updated" => "2023-05-08 23:43:47Z",
           "formerRepresentations" => %{
             "type" => "OrderedCollection",
             "orderedItems" => [
               %{"type" => "Note", "content" => "mew mew 1"}
             ],
             "totalItems" => 1
           }
         })
 
       {:ok, o} = Object.create(full_object1)
 
       assert {:ok, refetched} = Fetcher.refetch_object(o)
 
       assert %{
                "content" => "test 1",
                "formerRepresentations" => %{
                  "orderedItems" => [
                    %{"content" => "mew mew 1"}
                  ],
                  "totalItems" => 1
                }
              } = refetched.data
     end
 
     test "it goes through ObjectValidator and MRF", %{object2: object2} do
       with_mock Pleroma.Web.ActivityPub.MRF, [:passthrough],
         filter: fn
           %{"type" => "Note"} = object ->
             {:ok, Map.put(object, "content", "MRFd content")}
 
           arg ->
             passthrough([arg])
         end do
         {:ok, o} = Object.create(object2)
 
         assert {:ok, refetched} = Fetcher.refetch_object(o)
 
         assert %{"content" => "MRFd content"} = refetched.data
       end
     end
   end
 
+  describe "cross-domain redirect handling" do
+    setup do
+      mock(fn
+        # Cross-domain redirect with original domain in id
+        %{method: :get, url: "https://original.test/objects/123"} ->
+          %Tesla.Env{
+            status: 200,
+            url: "https://media.test/objects/123",
+            headers: [{"content-type", "application/activity+json"}],
+            body:
+              Jason.encode!(%{
+                "id" => "https://original.test/objects/123",
+                "type" => "Note",
+                "content" => "This is redirected content",
+                "actor" => "https://original.test/users/actor",
+                "attributedTo" => "https://original.test/users/actor"
+              })
+          }
+
+        # Cross-domain redirect with final domain in id
+        %{method: :get, url: "https://original.test/objects/final-domain-id"} ->
+          %Tesla.Env{
+            status: 200,
+            url: "https://media.test/objects/final-domain-id",
+            headers: [{"content-type", "application/activity+json"}],
+            body:
+              Jason.encode!(%{
+                "id" => "https://media.test/objects/final-domain-id",
+                "type" => "Note",
+                "content" => "This has final domain in id",
+                "actor" => "https://original.test/users/actor",
+                "attributedTo" => "https://original.test/users/actor"
+              })
+          }
+
+        # No redirect - same domain
+        %{method: :get, url: "https://original.test/objects/same-domain-redirect"} ->
+          %Tesla.Env{
+            status: 200,
+            url: "https://original.test/objects/different-path",
+            headers: [{"content-type", "application/activity+json"}],
+            body:
+              Jason.encode!(%{
+                "id" => "https://original.test/objects/same-domain-redirect",
+                "type" => "Note",
+                "content" => "This has a same-domain redirect",
+                "actor" => "https://original.test/users/actor",
+                "attributedTo" => "https://original.test/users/actor"
+              })
+          }
+
+        # Test case with missing url field in response (common in tests)
+        %{method: :get, url: "https://original.test/objects/missing-url"} ->
+          %Tesla.Env{
+            status: 200,
+            # No url field
+            headers: [{"content-type", "application/activity+json"}],
+            body:
+              Jason.encode!(%{
+                "id" => "https://original.test/objects/missing-url",
+                "type" => "Note",
+                "content" => "This has no URL field in response",
+                "actor" => "https://original.test/users/actor",
+                "attributedTo" => "https://original.test/users/actor"
+              })
+          }
+      end)
+
+      :ok
+    end
+
+    test "it rejects objects from cross-domain redirects with original domain in id" do
+      assert {:error, {:cross_domain_redirect, true}} =
+               Fetcher.fetch_and_contain_remote_object_from_id(
+                 "https://original.test/objects/123"
+               )
+    end
+
+    test "it rejects objects from cross-domain redirects with final domain in id" do
+      assert {:error, {:cross_domain_redirect, true}} =
+               Fetcher.fetch_and_contain_remote_object_from_id(
+                 "https://original.test/objects/final-domain-id"
+               )
+    end
+
+    test "it accepts objects with same-domain redirects" do
+      assert {:ok, data} =
+               Fetcher.fetch_and_contain_remote_object_from_id(
+                 "https://original.test/objects/same-domain-redirect"
+               )
+
+      assert data["content"] == "This has a same-domain redirect"
+    end
+
+    test "it handles responses without URL field (common in tests)" do
+      assert {:ok, data} =
+               Fetcher.fetch_and_contain_remote_object_from_id(
+                 "https://original.test/objects/missing-url"
+               )
+
+      assert data["content"] == "This has no URL field in response"
+    end
+  end
+
   describe "fetch with history" do
     setup do
       object2 = %{
         "id" => "https://mastodon.social/2",
         "actor" => "https://mastodon.social/users/emelie",
         "attributedTo" => "https://mastodon.social/users/emelie",
         "type" => "Note",
         "content" => "test 2",
         "bcc" => [],
         "bto" => [],
         "cc" => ["https://mastodon.social/users/emelie/followers"],
         "to" => [],
         "summary" => "",
         "formerRepresentations" => %{
           "type" => "OrderedCollection",
           "orderedItems" => [
             %{
               "type" => "Note",
               "content" => "orig 2",
               "actor" => "https://mastodon.social/users/emelie",
               "attributedTo" => "https://mastodon.social/users/emelie",
               "bcc" => [],
               "bto" => [],
               "cc" => ["https://mastodon.social/users/emelie/followers"],
               "to" => [],
               "summary" => ""
             }
           ],
           "totalItems" => 1
         }
       }
 
       mock(fn
         %{
           method: :get,
           url: "https://mastodon.social/2"
         } ->
           %Tesla.Env{
             status: 200,
             headers: [{"content-type", "application/activity+json"}],
             body: Jason.encode!(object2)
           }
 
         %{
           method: :get,
           url: "https://mastodon.social/users/emelie/collections/featured"
         } ->
           %Tesla.Env{
             status: 200,
             headers: [{"content-type", "application/activity+json"}],
             body:
               Jason.encode!(%{
                 "id" => "https://mastodon.social/users/emelie/collections/featured",
                 "type" => "OrderedCollection",
                 "actor" => "https://mastodon.social/users/emelie",
                 "attributedTo" => "https://mastodon.social/users/emelie",
                 "orderedItems" => [],
                 "totalItems" => 0
               })
           }
 
         env ->
           apply(HttpRequestMock, :request, [env])
       end)
 
       %{object2: object2}
     end
 
     test "it gets history", %{object2: object2} do
       {:ok, object} = Fetcher.fetch_object_from_id(object2["id"])
 
       assert %{
                "formerRepresentations" => %{
                  "type" => "OrderedCollection",
                  "orderedItems" => [%{}]
                }
              } = object.data
     end
   end
 end