Hidden files are no longer accessible with AJAX in Craft 3 I'm not sure if this is a bug, a security feature, or something to do with Twig 2; but since Craft 3 (RC3) I'm no longer able to access hidden twig files via ajax. 

This works fine:

  $.ajax({
    method:'GET',
    url:'/blog/test',
    success:function(data){
      $('body').append(data);
      console.log("SUCCESS", data);
    },
    error:function(data){
      console.log("ERROR", data);
    }
  });


However, when I point the url to a hidden file with this:

url:'/blog/_test',


I get a 404 (Not Found) error. For my example, I have both '_test.twig' and 'test.twig' files in the 'blog' directory.

Am I missing a breaking change? 



Update:

I didn't really know what it was for, but 'enableCsrfProtection' =&gt; true has always been set in my config/general.php file. (In both Craft 2 and 3). I've tried setting this to false too, and I'm getting the same results. 

Following Robins comment, I've attempted to follow these instructions: Enabling CSRF Protection.

I've set the global variables window.csrfTokenName and window.csrfTokenValue. And now I'm stuck. 

I'm pretty new to ajax, and I'm not sure how to extrapolate the example in a way where I can add the CSRF Token correctly in my 'GET' method.    

This is the response I'm getting: 

ERROR 
{readyState: 4, getResponseHeader: ƒ, getAllResponseHeaders: ƒ, setRequestHeader: ƒ, overrideMimeType: ƒ, …}
abort : (statusText)
always : ()
catch : (fn)
done : ()
fail : ()
getAllResponseHeaders : ()
getResponseHeader : (key)
overrideMimeType : (type)
pipe : ()
progress : ()
promise : (obj)
readyState : 4
responseText : "&lt;pre&gt;Not Found: Page not found.&lt;/pre&gt;"
setRequestHeader : (name,value)
state : ()
status : 404
statusCode : (map)
statusText : "Not Found"
then : (onFulfilled,onRejected,onProgress)
__proto__ : Object
My guess is still: you did not define a route correct route for your template. You can check if you can access the file when you call the route normally via url www.example.com/blog/test if you can see the template, then it should usually work, if not you did not define any routing rules see here

However: when you do it that way everyone with the link may see this template and I expect it to look not very good without content since it's probably only a partial, so I would highly recommend you to use the proper way via controller.

public function actionRenderTemplate(){
    $response = [
        'success'   =&gt; false,
        'message'   =&gt; 'something went wrong'
    ];
    if($template = Craft::$app-&gt;getRequest()-&gt;getBodyParam('template')){
        try{
            $response['success'] = true;
            $response['html'] = Craft::$app-&gt;getView()-&gt;renderTemplate($template, [
                'foo' =&gt; Craft::$app-&gt;getRequest()-&gt;getBodyParam('just an example to pass values');
            ]);
        }catch(\Twig_Error_Loader $e){
            $response['message'] = $e-&gt;getMessage(); 
        }catch(\yii\base\Exception $e){
            $response['message'] = $e-&gt;getMessage();
        }
    }
    return $this-&gt;asJson($response);
}


And your javascript

$.ajax({
    type: "post",
    data: {
        action: 'plugin-handle/controller/render-template',
        template: 'path/to/template'
    },
    url: '',
    success: function (data) {
        if (data.success === true) {
            // do your stuff
            $('body').append(data.html);
        } else {
            // something went wrong
            // show a message or whatever
        }
    }
});


Edit
Please make sure you are in the correct folder for rendering your template, there is a difference between frontend and cp requests docs that's why we always build a custom template service that checks if there is a template in the normal path first and only takes the plugin template if there is none so we can overwrite plugin templates if needed and don't need to check/overwrite the paths every time