Search code examples
androidruby-on-railsjsonandroid-volleyapi-design

How can I POST json data from android app to Ruby on Rails server


I am trying to send json data to my RoR server so that a new object is created when the POST request is received by the server. I am really new to RoR so I am not sure how to set this up correctly.

I have been searching other posts on here for a while applying solutions to similar problems but nothing I do seems to work.

On the Rails end of things - I have a route set up specifically for this purpose.

in routes.rb

post '/api' => 'expenses#post_json_expense'

in expenses_controller.rb

# For creating an expense from the android app
def post_json_expense
  Expense.new(expense_params)
end

# Never trust parameters from the scary internet, only allow the white list through.
def expense_params
  params.require(:expense).permit(:user_id, :amount, :category, :description)
end

I also turned off token authentication in case that is causing the problem.

in config/application

config.action_controller.allow_forgery_protection = false

On the android side of things, I am using Volley to send the POST request

private void saveExpense () {
        // Build the URL
        String url = "https://my-project-url.herokuapp.com/api";
        Log.i("API_REQUEST", url);

        EditText etAmount = findViewById(R.id.et_amount);
        EditText etDescription = findViewById(R.id.et_description);
        EditText etCategory = findViewById(R.id.et_category);

        // Get values from the EditTexts
        double amount = Double.parseDouble(etAmount.getText().toString());
        String description = etDescription.getText().toString().trim();
        String category = etCategory.getText().toString().trim();

        // If something was entered into all of the fields
        if (amount > 0 && !description.isEmpty() && !category.isEmpty()) {


            // Convert field entries into a JSON object
            JSONObject expenseData = new JSONObject();
            JSONObject expenseObject = new JSONObject();
            try {
                expenseData.put("user_id", user_id);
                expenseData.put("amount", amount);
                expenseData.put("description", description);
                expenseData.put("category", category);
                expenseObject.put("expense", expenseData);


            JsonObjectRequest jsonObjectRequest = new JsonObjectRequest(Request.Method.PUT, url, expenseObject, new Response.Listener<JSONObject>() {
                @Override
                public void onResponse(JSONObject response) {
                    Toast.makeText(EditExpenseActivity.this, "POST Successful", Toast.LENGTH_SHORT).show();
                    Intent intent = new Intent(EditExpenseActivity.this, MainActivity.class);
                    startActivity(intent);
                }
            }, new Response.ErrorListener() {
                @Override
                public void onErrorResponse(VolleyError error) {
                    Toast.makeText(EditExpenseActivity.this, "POST Unsuccessful", Toast.LENGTH_SHORT).show();
                }
            }) {
                @Override
                public Map<String, String> getHeaders() throws AuthFailureError {
                    Map<String, String> params = new HashMap<String, String>();
                    params.put("Content-Type", "application/json");
                    return params;
                }
            };

            // Make the API request
            RequestQueue queue = Volley.newRequestQueue(this);
            queue.add(jsonObjectRequest);


        }
    }

I keep getting a the error response each time I try to post data to the app. Where am I going wrong? I'm pretty new to rails is my first time trying to build an API so I am not sure what I am doing incorrectly.


Solution

  • Welcome to S.O.!

    So, there's a few things that need addressing here. For starters, in your controller:

    # For creating an expense from the android app
    def post_json_expense
      Expense.new(expense_params)
    end
    

    So, first, calling Expense.new here will only create a new object, but it will not persist it to the database; you'll also need to call save to do that.

    Next, you are not returning any sort of response back to the caller. Perhaps returning something like the id of the new Expense would be in order, or the expense itself. I'd suggest constructing the call like so:

    # For creating an expense from the android app
    def post_json_expense
      expense = Expense.new(expense_params)
      unless expense.save
        # TODO: Return an error status with some useful details
        # You can get some from expense.errors
        return render status: 400, json: { error: true }
      end
      # Render a JSON representation of the expense on successful save
      render json: expense.as_json
    end
    

    Next, on the client side, you are sending Content-type headers, which is good, but you could also send Accept headers, which clues the server as to what you expect to receive back:

    public Map<String, String> getHeaders() throws AuthFailureError {
      Map<String, String> params = new HashMap<String, String>();
      params.put("Content-Type", "application/json");
      params.put("Accept", "application/json");
      return params;
    }
    

    Finally, you have assigned the method to the POST route on your server:

    post '/api' => 'expenses#post_json_expense'
    

    But you are calling it as a PUT from your app:

    JsonObjectRequest jsonObjectRequest = new JsonObjectRequest(Request.Method.PUT, url, expenseObject, new Response.Listener<JSONObject>() {
    

    So, there is no PUT route at that URL, hence why the request would always fail.

    Cleaning up these issues should get you a successful response.

    Personally, I find using a simple utility like curl often helps debug these sort of communication errors, when you don't know whether the issue is the fault of an app-side coding issue or a server-side coding issue (or both). You can eliminate the variables by using something like curl which you can be confident works, then debug from there.

    Hope this helps you along!