Repository: ycm-core/ycmd

Issue:
ycmd should return valid JSON instead of empty HTML for 2 requests
/load_extra_conf_file and /ignore_extra_conf_file requests currently return an empty body
in case of success, which is not valid JSON. Instead, ycmd should return valid JSON body, for example just "true".



Discussion:
Please can you explain why you believe this change is necessary and generally useful? Is it causing you a problem?

FWIW we have some in-progress API documentation here: http://puremourning.github.io/ycmd-1/

> Please can you explain why you believe this change is necessary and generally useful?

Well, if ycmd wants to have a clean and consistent API, then all replies should follow some kind of rule. Considering almost all replies already are in JSON format, even errors, it may not be unreasonable to think that ALL replies should be JSON.
Even the documentation link _you_ gave says the results of the request should be JSON :)
Allowing YCM clients to assume all ycmd replies are in the same format (JSON) can simplify clients code as you can imagine. (I assume it must not be too complicated for ycmd to just return 4 bytes ("true") instead of one)

This is the kind of detail that differenciates a good, predictable API from an average API, with gotchas awaiting at the corner.

Nobody is going to deny the "cleanliness of API" argument. I was just curious as to whether there was a more pressing reason to make a (potentially breaking) API change (such as you're trying to do X and Y is not working). Changing the API for cosmetic reasons is hard to justify :/

IIRC this has come up before, though I forget the context.

The change we're talking about is really simple https://github.com/vheon/ycmd/commit/17cf01a9ee911189d1b9c5aaf99a230b1d8d35f4. So we only need to decide if it really makes sense.

I'm not against this change. I can't see how it would be backwards incompatible; we weren't returning anything in the body before and I can't imagine how any client would break if the body they were ignoring before suddenly starting having content. The only thing clients did up to now is look at the status code.

So IMO this should be fine. We can just make these handlers return `{}` and be done with it.

If someone can make a convincing case that this would be a breaking change, then of course we wouldn't do it. It's entirely a cosmetic improvement.


Resolving patch:
diff --git a/ycmd/handlers.py b/ycmd/handlers.py
--- a/ycmd/handlers.py
+++ b/ycmd/handlers.py
@@ -188,6 +188,8 @@ def LoadExtraConfFile():
   request_data = RequestWrap( request.json, validate = False )
   extra_conf_store.Load( request_data[ 'filepath' ], force = True )
 
+  return _JsonResponse( True )
+
 
 @app.post( '/ignore_extra_conf_file' )
 def IgnoreExtraConfFile():
@@ -195,6 +197,8 @@ def IgnoreExtraConfFile():
   request_data = RequestWrap( request.json, validate = False )
   extra_conf_store.Disable( request_data[ 'filepath' ] )
 
+  return _JsonResponse( True )
+
 
 @app.post( '/debug_info' )
 def DebugInfo():


Test patch:
diff --git a/ycmd/tests/misc_handlers_test.py b/ycmd/tests/misc_handlers_test.py
--- a/ycmd/tests/misc_handlers_test.py
+++ b/ycmd/tests/misc_handlers_test.py
@@ -24,10 +24,10 @@
 standard_library.install_aliases()
 from builtins import *  # noqa
 
-from nose.tools import ok_
-from hamcrest import assert_that, contains
+from hamcrest import assert_that, contains, empty, equal_to, has_entries
+import requests
 
-from ycmd.tests import SharedYcmd
+from ycmd.tests import PathToTestFile, SharedYcmd
 from ycmd.tests.test_utils import BuildRequest, DummyCompleter, PatchCompleter
 
 
@@ -35,7 +35,9 @@
 def MiscHandlers_SemanticCompletionAvailable_test( app ):
   with PatchCompleter( DummyCompleter, filetype = 'dummy_filetype' ):
     request_data = BuildRequest( filetype = 'dummy_filetype' )
-    ok_( app.post_json( '/semantic_completion_available', request_data ).json )
+    assert_that( app.post_json( '/semantic_completion_available',
+                                request_data ).json,
+                 equal_to( True ) )
 
 
 @SharedYcmd
@@ -43,17 +45,26 @@ def MiscHandlers_EventNotification_AlwaysJsonResponse_test( app ):
   event_data = BuildRequest( contents = 'foo foogoo ba',
                              event_name = 'FileReadyToParse' )
 
-  app.post_json( '/event_notification', event_data ).json
+  assert_that( app.post_json( '/event_notification', event_data ).json,
+               empty() )
 
 
 @SharedYcmd
 def MiscHandlers_EventNotification_ReturnJsonOnBigFileError_test( app ):
-  # We generate a content greater than Bottle.MEMFILE_MAX, which is set to 1Mb.
+  # We generate a content greater than Bottle.MEMFILE_MAX, which is set to 1MB.
   contents = "foo " * 500000
   event_data = BuildRequest( contents = contents,
                              event_name = 'FileReadyToParse' )
 
-  app.post_json( '/event_notification', event_data, expect_errors = True ).json
+  response = app.post_json( '/event_notification',
+                            event_data,
+                            expect_errors = True )
+  assert_that( response.status_code,
+               equal_to( requests.codes.request_entity_too_large ) )
+  assert_that( response.json,
+               has_entries( { 'traceback': None,
+                              'message': 'None',
+                              'exception': None } ) )
 
 
 @SharedYcmd
@@ -71,3 +82,21 @@ def MiscHandlers_FilterAndSortCandidates_Basic_test( app ):
   response_data = app.post_json( '/filter_and_sort_candidates', data ).json
 
   assert_that( response_data, contains( candidate2, candidate3 ) )
+
+
+@SharedYcmd
+def MiscHandlers_LoadExtraConfFile_AlwaysJsonResponse_test( app ):
+  filepath = PathToTestFile( '.ycm_extra_conf.py' )
+  extra_conf_data = BuildRequest( filepath = filepath )
+
+  assert_that( app.post_json( '/load_extra_conf_file', extra_conf_data ).json,
+               equal_to( True ) )
+
+
+@SharedYcmd
+def MiscHandlers_IgnoreExtraConfFile_AlwaysJsonResponse_test( app ):
+  filepath = PathToTestFile( '.ycm_extra_conf.py' )
+  extra_conf_data = BuildRequest( filepath = filepath )
+
+  assert_that( app.post_json( '/ignore_extra_conf_file', extra_conf_data ).json,
+               equal_to( True ) )
diff --git a/ycmd/tests/testdata/.ycm_extra_conf.py b/ycmd/tests/testdata/.ycm_extra_conf.py
new file mode 100644