Can not assign to conn.assigns

phoenix
troubleshooting

#1

Hi everyone, I’m making a shortener and the idea is that after somebody POST their url, it redirects them to “api/shorturl/new/” from “/” and then they receive the content of the %Short structure.

To hold the values inside %Short I’m trying to assign a map to conn.assigns but it doesn’t seem to work.

My controller:

defmodule ShortenerWeb.UrlController do
    use ShortenerWeb, :controller

    alias Shortener.Api
    alias Shortener.Urls.Short

    def index(conn, _) do
        if conn.assigns == %{} do
             conn |> json(%{error: "Wrong Format"})
        else
            conn |> json(conn.assigns[:short])

        end
    end

    def show(conn, id) do
        id
    end

    def create(conn, %{"short" => %{"original_url" => original_url}}) do
        {:ok, short} = Api.short(original_url)
        conn =  assign(conn, :short, Map.from_struct(short))
        redirect(conn, to: Routes.url_path(conn, :index))

    end

end

The shortener structure

defmodule Shortener.Urls.Short do
    use Ecto.Schema
    import Ecto.Changeset

    schema "urls" do
        field :original_url, :string
    end

    def changeset(short, attrs \\ %{}) do
        short
        |> cast(attrs, [:original_url])
        |> validate_required([:original_url])
    end
end

Here’s part of my Router just in case:

defmodule ShortenerWeb.Router do
use ShortenerWeb, :router

...
  scope "/api/shorturl/new", ShortenerWeb do
    pipe_through :api

    get "/", UrlController, :index
    post "/", UrlController, :create
    get "/:id", UrlController, :show
  end

  scope "/", ShortenerWeb do
    pipe_through :browser

    get "/", PageController, :index

  end

end

Thank you!


#2

It looks like you are assigning things to conn in the create action and then expecting them to exist in the index action? This is not how connections work. Basically each conn resembles one request. When the user’s browser makes a new request (after being redirected to index's URL), a new connection structure is created and the old assigns no longer exist.

If you wish to keep data in a user’s session over many requests, check out Plug’s *_session functions: https://hexdocs.pm/plug/search.html?q=session


#3

Thank you @Nicd, I made it work with sessions, here’s my code if anyone is interested:

defmodule ShortenerWeb.UrlController do
    use ShortenerWeb, :controller

    alias Shortener.Api
    alias Shortener.Urls.Short

    def index(conn, _) do
        conn = fetch_session(conn)
        map = get_session(conn, :short)
        
        if is_number(map.id) do
            conn |> json(%{
                original_url: map.original_url,
                short_url: map.id
            })
            
        else
            conn |> json(%{error: "Wrong Format"})

        end
    end

    def show(conn, id) do
        id
    end

    def create(conn, %{"short" => %{"original_url" => original_url}}) do
        {:ok, short} = Api.short(original_url)
        conn = fetch_session(conn)
        conn =  put_session(conn, :short, Map.from_struct(short))
        redirect(conn, to: Routes.url_path(conn, :index))

    end

end

#4

Glad you had it to work, here are a few comments to help you better master the language…

  1. I personally don’t think the pipe operator is helpful for 1-liner

    conn |> json(%{error: "Wrong Format"})
    

    Should be:

    json(conn, %{error: "Wrong Format"})
    
  2. On the other hand, it was meant to express (and greatly simplify syntax) in sequential data flow:

    conn = fetch_session(conn)
    conn =  put_session(conn, :short, Map.from_struct(short))
    redirect(conn, to: Routes.url_path(conn, :index)
    

    Should be:

    conn
    |> fetch_session()
    |> put_session(:short, Map.from_struct(short))
    |> redirect(to: Routes.url_path(conn, :index)
    

#5

Yes, I agree it’s not very helpful in one-liners. Thank you for your comments, I’ll format my code so that it looks better :grin: